diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..c4dd004
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,42 @@
+name: Deploy static site to GitHub Pages
+
+on:
+ push:
+ branches:
+ - pyodide # Triggers the workflow on pushes to the pyodide branch
+ workflow_dispatch: # Allows manual triggering from the GitHub UI
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout your repository
+ uses: actions/checkout@v4
+ # Add steps to build your static site (e.g., npm run build)
+ - name: Run deployment script
+ run: |
+ export PYODIDE_VERSION=0.29.3
+ export TARGET_DIR=dist
+ ./deploy_calculator.sh
+ - name: Upload artifact
+ # The contents of the 'build' directory will be uploaded as an artifact
+ uses: actions/upload-pages-artifact@v4
+ with:
+ path: 'dist/' # Change this to your build output directory (e.g., public, dist)
+
+ deploy:
+ # Add a dependency to the build job
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write # Grants the GITHUB_TOKEN the necessary permissions to deploy to GitHub Pages
+ id-token: write # Required for OIDC authentication
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4 # This action handles the deployment
diff --git a/.gitignore b/.gitignore
index a0eb841..02a01ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,7 @@ endf/MT.DAT
endf/PLOT.CHR
endf/PLOT.SYM
endf/*.out
+
+# pyodide build cruft
+activation/pyodide
+activation/periodictable_wheel_name.txt
diff --git a/README.md b/README.md
index d78a1a9..3f13508 100644
--- a/README.md
+++ b/README.md
@@ -13,14 +13,8 @@ available on [github](https://github.com/pkienzle/periodictable).
Installation
============
-The activation web frontend is in the activation subdirectory and the cgi
-backend is in cgi-bin. Update the server with something like:
-
- sudo cp -rp activation/* /var/www/resources/activation
- sudo cp -p cgi-bin/nact.py /var/www/cgi-bin
-
-The web page uses the date of activate/index.html to show the last
-modification date on the program, so be sure to preserve attributes in copy.
+The activation web frontend is in the activation subdirectory.
+The backend API is in the cgi-bin folder (nact.py)
Be sure the web server is configured to use python 3, with the periodictable
package updated to the latest version:
@@ -33,7 +27,8 @@ of updates this will also set the last modification date on index.html.
For testing you can run the server from the repository:
- python server.py [host | host:port]
+ pip install flask
+ python flask_server.py [host | host:port]
Additional files:
@@ -41,7 +36,7 @@ Additional files:
needed unless you wish to update the graphs, for example, when new versions of
the endf database are released.
-* server.py is used to run a test server for debugging the web application, or
+* flask_server.py is used to run a test server for debugging the web application, or
showing potential new features to users. See the help inside the file for
details on running the server.
@@ -50,6 +45,19 @@ Additional files:
* cgi-bin/hello.py is a minimal test script for python cgi.
+Pyodide implementation
+======================
+You can make a serverless install using pyodide to run the backend API. The `deploy_calculator.sh`
+script will install to `/var/html/resources/activation/index.html`.
+
+To test the pyodide version before deploying, install into a temporary directory:
+```sh
+TARGET_DIR=/tmp/pt bash deploy_calculator.sh
+(cd /tmp/pt && python -m http.server)
+```
+You can then navigate to http://localhost:8000/index.html to view the application.
+
+
Backend interface
=================
@@ -84,6 +92,25 @@ request = {
}
```
+```python
+python_request = {
+ 'calculate': "all", # target is "scattering" or "activation" or "all"
+ 'sample': 'Co', # Material
+ 'flux': '100000', # Thermal flux
+ 'Cd': '0', # Cd ratio
+ 'fast': '0', # Thermal/fast ratio
+ 'mass': '0', # Mass
+ 'exposure': '1', # Time on beam
+ 'rest': ["0","1","24","360"], # Time off beam
+ 'density': '0', # Density
+ 'thickness': '1', # Thickness
+ 'wavelength': '1', # Source neutrons
+ 'xray': 'Cu Ka', # Source Xrays
+ 'decay': '0.001', # target for "Time to decay below"
+ 'abundance': 'IAEA' # natural abundance tables (IAEA or NIST)
+}
+```
+
The response is a JSON object with the following fields
```javascript
response = {
@@ -148,119 +175,125 @@ Example
-------
```sh
-$ curl -s -d "sample=Co" -X POST https://www.ncnr.nist.gov/cgi-bin/nact.py | python -m json.tool
+$ curl -s -d '{"sample": "Co"}' -H "Content-Type: application/json" -X POST http://localhost:8008/api/calculate | python -m json.tool
{
- "sample": {
- "name": "Co",
- "density": 8.9,
- "natural_density": 8.9,
- "thickness": 1.0,
- "mass": 1.0,
- "formula": "Co"
- },
"activation": {
- "flux": 100000.0,
- "decay_level": 0.001,
- "total": [
- 0.5088248094656863,
- 0.009706843055148993,
- 1.550197781340068e-05,
- 1.5423998799711213e-05
- ],
- "rest": [
- 0,
- 1,
- 24,
- 360
- ],
+ "Cd": 0.0,
"activity": [
{
- "reaction": "act",
- "product": "Co-60",
- "halflife": "5.272 y",
- "comments": "s for 10m isomer added to ground state",
+ "comments": "",
+ "halflife": "10.5 m",
+ "isotope": "Co-59",
"levels": [
- 1.550756280503848e-05,
- 1.5507330056885684e-05,
- 1.5501977813400642e-05,
- 1.5423998799711213e-05
+ 0.5087467869376632,
+ 0.009690144997026036,
+ 2.6447704770864502e-42,
+ 0.0
],
- "isotope": "Co-59"
+ "product": "Co-60m+",
+ "reaction": "act"
},
{
- "reaction": "act",
- "product": "Co-60m+",
- "halflife": "10.5 m",
- "comments": "",
+ "comments": "Co-61 prod from Co-60m only",
+ "halflife": "1.65 h",
+ "isotope": "Co-59",
"levels": [
- 0.5088093019028804,
- 0.009691335725091536,
- 2.6450954673146495e-42,
- 0.0
+ 7.305869373119584e-16,
+ 4.799870068457319e-16,
+ 3.0552994658480865e-20,
+ 1.52897407497221e-81
],
- "isotope": "Co-59"
+ "product": "Co-61",
+ "reaction": "2n"
},
{
- "reaction": "2n",
- "product": "Co-61",
- "halflife": "1.65 h",
- "comments": "Co-61 prod from Co-60m only",
+ "comments": "s for 10m isomer added to ground state",
+ "halflife": "5.272 y",
+ "isotope": "Co-59",
"levels": [
- 7.306767120646388e-16,
- 4.800459878001244e-16,
- 3.055674902007567e-20,
- 1.5291619557875176e-81
+ 1.5505657464889658e-05,
+ 1.5505424745333514e-05,
+ 1.5500073159453058e-05,
+ 1.5422103726672467e-05
],
- "isotope": "Co-59"
+ "product": "Co-60",
+ "reaction": "act"
},
{
- "reaction": "2n",
- "product": "Co-61",
- "halflife": "1.65 h",
"comments": "Co-61 prod assuming all Co-60m has decayed to Co-60",
+ "halflife": "1.65 h",
+ "isotope": "Co-59",
"levels": [
- 1.3649499897275873e-16,
- 8.967560554449202e-17,
- 5.7081926346341585e-21,
- 2.8565705754412276e-82
+ 1.3647822848511002e-16,
+ 8.966458753177e-17,
+ 5.707491296308241e-21,
+ 2.8562196022780084e-82
],
- "isotope": "Co-59"
+ "product": "Co-61",
+ "reaction": "2n"
}
],
+ "decay_level": 0.001,
+ "decay_time": 1.5773360047132599,
"exposure": 1.0,
- "decay_time": 1.5773675158317233,
"fast": 0.0,
- "Cd": 0.0
+ "flux": 100000.0,
+ "rest": [
+ 0,
+ 1,
+ 24,
+ 360
+ ],
+ "total": [
+ 0.508762292595129,
+ 0.00970565042177194,
+ 1.5500073159453095e-05,
+ 1.5422103726672467e-05
+ ]
+ },
+ "sample": {
+ "density": 8.9,
+ "formula": "Co",
+ "formula_latex": "Co",
+ "mass": 1.0,
+ "name": "Co",
+ "natural_density": 8.9,
+ "thickness": 1.0
},
"scattering": {
- "sld": {
- "real": 2.2645416201426363,
- "imag": 0.009403091502484154,
- "incoh": 5.632988294016107
+ "contrast_match": {
+ "D2O_fraction": 0.4066002243307043,
+ "sld": 2.2645414633790257
},
- "xs": {
- "coh": 0.07085810248318081,
- "abs": 1.8806183004968307,
- "incoh": 0.43843639843243204
- },
- "penetration": 0.4184253079898973,
"neutron": {
- "wavelength": 1.0,
- "energy": 81.80420235572412,
- "velocity": 3956.0339760560055
+ "energy": 81.80421023488275,
+ "velocity": 3956.0340061039888,
+ "wavelength": 1.0
},
- "transmission": 9.163767420488476
+ "penetration": 0.4184253369555237,
+ "sld": {
+ "imag": 0.009403090851552168,
+ "incoh": 5.632980055822083,
+ "real": 2.2645414633790257
+ },
+ "transmission": 9.16376893656492,
+ "xs": {
+ "abs": 1.8806181703104334,
+ "coh": 0.07085931929382916,
+ "incoh": 0.4384351463657107
+ }
},
+ "success": true,
+ "version": "2.0.2",
"xray_scattering": {
- "xray": {
- "wavelength": 1.5418,
- "energy": 8.041522080237366
- },
"sld": {
- "real": 63.020248367719645,
- "imag": 9.14097379704212
+ "imag": 9.140742563282087,
+ "real": 63.02025244915057
+ },
+ "xray": {
+ "energy": 8.041522793695698,
+ "wavelength": 1.5418
}
- },
- "success": true
+ }
}
```
diff --git a/activation/api_fetch.js b/activation/api_fetch.js
new file mode 100644
index 0000000..12b8368
--- /dev/null
+++ b/activation/api_fetch.js
@@ -0,0 +1,25 @@
+class FetchAPI {
+ response_callback = null;
+ url = null;
+
+ initialize (response_callback, ready_callback=() => {}, url="/api/calculate") {
+ this.response_callback = response_callback;
+ this.url = url;
+ ready_callback();
+ }
+
+ submit(data) {
+ fetch(this.url, {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(response => response.json())
+ .then(json => this.response_callback(json))
+ .catch(error => this.response_callback({'success':false,'detail':{'fetch error':error}}));
+ }
+}
+
+const API = new FetchAPI();
\ No newline at end of file
diff --git a/activation/api_webworker.js b/activation/api_webworker.js
new file mode 100644
index 0000000..61e469c
--- /dev/null
+++ b/activation/api_webworker.js
@@ -0,0 +1,29 @@
+class WebWorkerAPI {
+ response_callback = null;
+ ready_callback = null;
+ url = null;
+ pyodideWorker = null;
+
+ initialize (response_callback, ready_callback=() => {}, url="./webworker.js") {
+ this.response_callback = response_callback;
+ this.ready_callback = ready_callback;
+ this.url = url;
+ this.pyodideWorker = new Worker(this.url, {type: "module"});
+ this.pyodideWorker.onmessage = (event) => {
+ if ('worker_ready' in event.data) {
+ this.ready_callback();
+ }
+ else {
+ this.response_callback(event.data);
+ }
+ }
+ }
+
+ submit(data) {
+ this.pyodideWorker.postMessage({
+ data: data
+ });
+ }
+}
+
+const API = new WebWorkerAPI();
diff --git a/activation/calculator_main.js b/activation/calculator_main.js
new file mode 100644
index 0000000..051c59b
--- /dev/null
+++ b/activation/calculator_main.js
@@ -0,0 +1,452 @@
+// API comes in from webworker_api.js or form_api.js above
+// JQuery ($) is loaded in a script tag as well
+
+// standard year for activation calculations is 365, not 365.2425
+const HOURS_PER_YEAR = 365*24; // = 365.2425*24;
+const ELEMENTS = {"n": 0, "H": 1, "He": 2, "Li": 3, "Be": 4, "B": 5, "C": 6, "N": 7, "O": 8, "F": 9, "Ne": 10, "Na": 11, "Mg": 12, "Al": 13, "Si": 14, "P": 15, "S": 16, "Cl": 17, "Ar": 18, "K": 19, "Ca": 20, "Sc": 21, "Ti": 22, "V": 23, "Cr": 24, "Mn": 25, "Fe": 26, "Co": 27, "Ni": 28, "Cu": 29, "Zn": 30, "Ga": 31, "Ge": 32, "As": 33, "Se": 34, "Br": 35, "Kr": 36, "Rb": 37, "Sr": 38, "Y": 39, "Zr": 40, "Nb": 41, "Mo": 42, "Tc": 43, "Ru": 44, "Rh": 45, "Pd": 46, "Ag": 47, "Cd": 48, "In": 49, "Sn": 50, "Sb": 51, "Te": 52, "I": 53, "Xe": 54, "Cs": 55, "Ba": 56, "La": 57, "Ce": 58, "Pr": 59, "Nd": 60, "Pm": 61, "Sm": 62, "Eu": 63, "Gd": 64, "Tb": 65, "Dy": 66, "Ho": 67, "Er": 68, "Tm": 69, "Yb": 70, "Lu": 71, "Hf": 72, "Ta": 73, "W": 74, "Re": 75, "Os": 76, "Ir": 77, "Pt": 78, "Au": 79, "Hg": 80, "Tl": 81, "Pb": 82, "Bi": 83, "Po": 84, "At": 85, "Rn": 86, "Fr": 87, "Ra": 88, "Ac": 89, "Th": 90, "Pa": 91, "U": 92, "Np": 93, "Pu": 94, "Am": 95, "Cm": 96, "Bk": 97, "Cf": 98, "Es": 99, "Fm": 100, "Md": 101, "No": 102, "Lr": 103, "Rf": 104, "Db": 105, "Sg": 106, "Bh": 107, "Hs": 108, "Mt": 109, "Ds": 110, "Rg": 111, "Cn": 112, "Nh": 113, "Fl": 114, "Mc": 115, "Lv": 116, "Ts": 117, "Og": 118};
+let ACTIVE_BUTTON = "activation";
+// Parse environment variables "index.html?cutoff=...&abundance=..."
+function getURLParameter(name) {
+ var value = decodeURIComponent(
+ (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[,null])[1]
+ );
+ return value;
+}
+// From Doron Grinzaig (2017-11-21)
+// https://stackoverflow.com/questions/14129953/how-to-encode-a-string-in-javascript-for-displaying-in-html
+function sanitize(html) {
+ return $('
').text(html).html();
+}
+function format_activation_value(v, cutoff) {
+ return (v
= 1000.) return v.toFixed(0)+' g';
+ else if (v >= 1.) return v.toFixed(3)+' g';
+ else if (v >= 1e-3) return (1e3*v).toFixed(3)+' mg';
+ else if (v >= 1e-6) return (1e6*v).toFixed(3)+' ug';
+ else if (v >= 1e-9) return (1e9*v).toFixed(3)+' ng';
+ else return v.toExponential(4)+' ng';
+}
+function format_time(v) {
+ if (v >= HOURS_PER_YEAR) return nice_number(v/HOURS_PER_YEAR)+' yrs';
+ else if (v >= 48 || v == 24) return nice_number(v/24)+' days';
+ else if (v >= 2 || v == 1) return nice_number(v)+' hrs';
+ else if (v >= 2/60. | v == 1/60.) return nice_number(v*60)+' min';
+ else return (v*3600).toPrecision(3)+' sec';
+}
+function parse_half_life(s) {
+ var y = HOURS_PER_YEAR*3600;
+ var units = {Gy: 1e9*y, My: 1e6*y, ky: 100*y, y: y, d: 24*3600, h: 3600, m: 60, s: 1};
+ var a = s.split(" ");
+ var value_str = a[0], units_str = a[1];
+ return parseFloat(value_str) * units[units_str];
+}
+function parse_isotope(s) {
+ var a = s.split("-");
+ var element = a[0], isotope = a[1];
+ return ELEMENTS[element] + parseInt(isotope)/1000;
+}
+$.tablesorter.addParser({
+ id: 'isotope',
+ is: function (s) { return /^ *[A-Z][a-z]?-[0-9]+[sm+]* *$/.test(s); },
+ format: function (s) { return parse_isotope(s); },
+ type: 'numeric'
+});
+$.tablesorter.addParser({
+ id: 'half-life',
+ is: function (s) { return /^ *[0-9.+-eE]+ *[kMG]?[smhdy] *$/.test(s); },
+ format: function (s) { return parse_half_life(s); },
+ type: 'numeric'
+});
+$.tablesorter.addParser({
+ id: 'float',
+ is: function (s) { return /^ *[0-9.+-eE]+ *$/.test(s); },
+ format: function (s) { return s == '---' ? 0. : parseFloat(s); },
+ type: 'numeric'
+});
+
+function parse_url_parameters() {
+ // Parse URL parameters.
+ var cutoff_par = getURLParameter('cutoff');
+ if (cutoff_par==='null') CUTOFF = 0.0005;
+ else CUTOFF = Number(cutoff_par);
+ if (isNaN(CUTOFF)) { // IE doesn't support Number.isNaN
+ show_error("cutoff="+sanitize(cutoff_par)+" in URL should be floating point μCi; default is 0.0005.
");
+ CUTOFF = 0.0005;
+ }
+ CUTOFF_Bq = CUTOFF*37000;
+
+ var decay_par = getURLParameter('decay');
+ if (decay_par==='null') DECAY_LEVEL = CUTOFF>0?CUTOFF:0.0005;
+ else DECAY_LEVEL = Number(decay_par)
+ if (isNaN(DECAY_LEVEL)) { // IE doesn't support Number.isNaN
+ show_error("decay="+sanitize(decay_par)+" in URL should be floating point μCi; default is cutoff.
");
+ DECAY_LEVEL = CUTOFF>0?CUTOFF:0.0005;
+ }
+
+ var abundance_par = getURLParameter('abundance');
+ if (abundance_par==='null') ABUNDANCE = "IAEA";
+ // CRUFT: periodictable no longer uses NIST 2001 data for abundance
+ else if (abundance_par==='NIST') ABUNDANCE = "IUPAC";
+ else ABUNDANCE = abundance_par;
+ if (ABUNDANCE!=="IUPAC" && ABUNDANCE!=="IAEA") {
+ show_error("abundance="+sanitize(abundance_par)+" in URL should be IUPAC or IAEA; default is IAEA
");
+ ABUNDANCE = "IAEA";
+ }
+}
+
+function enable_forms() {
+ // Enable forms; they are disabled by default so that noscript users
+ // can't do anything.
+ $('.container').removeClass("disabled");
+ $('#id_chemical_formula').removeAttr("disabled");
+ $('#id_submit_scat').removeAttr("disabled");
+ $('#id_submit_act').removeAttr("disabled");
+ $('a').each(function(){ this.tabIndex = -1; });
+ $('.help').each(function(){ $(this).append(" "); });
+ $('input[type=text]').keydown(function(e) {
+ if (e.keyCode == 13) {
+ e.preventDefault();
+ submit_query(ACTIVE_BUTTON);
+ }
+ });
+ $('input[type=text]').focus(function(e) {
+ var panel = $(this).closest('.panel')[0].id;
+ if (panel == "scattering-panel") {
+ ACTIVE_BUTTON = "scattering";
+ } else if (panel == "activation-panel") {
+ ACTIVE_BUTTON = "activation";
+ }
+ } );
+
+ //$("#id-activationForm").submit(submit_query);
+ $("#id-activationForm").submit(function(event) {event.preventDefault();});
+ $('#id_submit_scat').click(function() { submit_query('scattering'); });
+ $('#id_submit_act').click(function() { submit_query('activation'); });
+}
+
+function submit_query(target){
+ ACTIVE_BUTTON = target;
+
+ // activate the loader spinner:
+ document.querySelector("div.loader").classList.remove("not-started");
+
+ const form = document.getElementById("id-activationForm");
+ const formData = new FormData(form);
+ const initial_data = Object.fromEntries(formData.entries());
+ const { time_off, ...data } = initial_data; // destructuring assignment to remove time_off from data
+
+ // populate rest array with time_off from initial data
+ data.rest = ["0","1","24","360",time_off];
+
+ const query = {
+ calculate: target,
+ decay: DECAY_LEVEL,
+ abundance: ABUNDANCE,
+ ...data
+ };
+ API.submit(query);
+}
+
+function cssrule(class_name){
+ var ss = document.styleSheets;
+ for (var i=0; i= cutoff);
+ columns.push(row.levels[j])
+ }
+ if (active) data.rows.push(columns);
+ }
+
+ var activity_header, activity_button;
+ if (cutoff>0) {
+ activity_header = 'Activity (μCi) above ' + cutoff.toExponential(4) + ' μCi';
+ activity_button = 'All ';
+ } else if (CUTOFF>0) {
+ activity_header = 'Activity (μCi)';
+ activity_button = '>'+CUTOFF.toExponential(4)+' μCi ';
+ } else {
+ activity_header = 'Activity (μCi)';
+ activity_button = '';
+ }
+ //add headers to table
+ content += '\n \n';
+ content += ' \n';
+ content += ' \n \n \n'
+ var some_unusual = false;
+ for (var i in data.rows) {
+ var row_style = '';
+ if (data.rows[i][1] === 'b') {
+ some_unusual = true;
+ row_style = 'activity_unusual';
+ } else if (data.rows[i][2].slice(-1) === 't') {
+ some_unusual = true;
+ row_style = 'activity_unusual';
+ //} else if (data.rows[i][2].slice(-1) === 's') {
+ // row_style = 'activity_normal';
+ } else {
+ row_style = 'activity_normal';
+ }
+ content += ` `;
+ for (var j=0; j =4) {
+ content += ''+format_activation_value(data.rows[i][j], cutoff)+' ';
+ } else {
+ content += '' + data.rows[i][j] + ' ';
+ }
+ }
+ content += ' \n';
+ }
+ content += ' \n \n \n';
+ if (some_unusual) {
+ content += ' b reaction activity is slightly underestimated; it does not include decay of transients after removal from beam. \n';
+ }
+ content += ' \n
\n';
+ return content;
+}
+function show_error(error_html) {
+ var content = '
\n';
+ content += error_html;
+ content += '\n';
+ $(content).prependTo('div[id=results]');
+}
+function process_response(ldata) {
+ //console.log(ldata);
+ if (typeof(ldata)==="string") ldata=$.parseJSON(ldata);
+ if (!ldata.success) {
+ var error_html = '';
+ var keys = Object.keys(ldata.detail);
+ keys.sort();
+ for (i = keys.length-1; i >= 0; i--) {
+ error_html += 'Error ' +sanitize(keys[i])+': '+sanitize(ldata.detail[keys[i]])+' \n';
+ }
+ show_error(error_html);
+ return;
+ }
+
+ var act = ldata.activation;
+ var sample = ldata.sample;
+ var scat = ldata.scattering;
+ var xscat = ldata.xray_scattering;
+ var content = '\n';
+
+ if (typeof act == 'undefined') {
+ // do nothing if not returned
+ } else if ("error" in act) {
+ // Restricted title
+ content += '
activation calculation failed with
\n'+sanitize(act.error)+' \n';
+ } else {
+ // Full title
+ content += '
Activation of ' + sample.name + ' after ' + format_time(act.exposure)+' at '+act.flux.toPrecision(3)+' n/cm2 /s \n';
+
+ // Disclaimer
+ content += '
Estimated activation only. All samples must be evaluated by NIST Health Physics to determine if and how the sample can be removed from the NCNR.
';
+
+ // Atoms
+ //var formula = sample.formula;
+ var formula = sanitize(sample.formula_latex).replace(/\$_{([^}]*)}\$/g, '
$1 ');
+ content += '
Sample in beam: ' + format_mass(sample.mass) + ' of ' + formula + '\n';
+
+ // Rabbit parameters
+ if (act.Cd > 0 || act.fast > 0) {
+ content += ' Rabbit system: Cd ratio = ' + act.Cd + ', thermal/fast ratio = ' + act.fast + '\n'
+ }
+
+ // Decay time
+ if (act.decay_time > 0) {
+ content += ' Time to decay below '+act.decay_level.toExponential(4)+' μCi is '+format_time(act.decay_time)+'.\n';
+ }
+ content += '
\n'
+
+ // Activation table
+ if (CUTOFF == 0.) {
+ content += activation_table(act, 0.);
+ } else {
+ content += '
\n' + activation_table(act, CUTOFF) + '
\n';
+ content += '
\n' + activation_table(act, 0.) + '
\n';
+ }
+ }
+
+
+ if (typeof scat == 'undefined') {
+ // do nothing if not returned
+ } else if ("error" in scat) {
+ content += '
neutron scattering calculation failed with
\n'+sanitize(scat.error)+' \n';
+ } else if ("error" in xscat) {
+ content += '
X-ray scattering calculation failed with
\n'+sanitize(xscat.error)+' \n';
+ } else {
+ // Drop coherent cross section from penetration depth calculation.
+ // An alternative formulation, based on total scattering b, would be
+ // penetration = 1/(1/scat.penetration - scat.xs.coh), which perhaps
+ // takes diffuse coherent scattering into account [Glinka 2011].
+ var penetration = 1 /(scat.xs.abs+scat.xs.incoh);
+ var transmission = 100*Math.exp(-sample.thickness/penetration);
+ var flux_str = $('input:text[id=id_flux]').val();
+ var transmitted_flux = parseFloat(flux_str)*transmission/100.0;
+
+ // Mass density
+ var density_str = $('input:text[id=id_density]').val();
+ content += "
Scattering from " + sanitize(sample.name) + " ";
+
+ // Source neutrons
+ content += "
Source neutrons: "
+ + scat.neutron.wavelength.toFixed(3) + " Å"
+ + " = "+scat.neutron.energy.toFixed(2) + " meV"
+ + " = "+scat.neutron.velocity.toFixed(0) + " m/s ";
+ content += "Source X-rays: "
+ + xscat.xray.wavelength.toFixed(3) + " Å"
+ + " = " + xscat.xray.energy.toFixed(3) + " keV ";
+ // Atoms
+ //var formula = sample.formula;
+ var formula = sanitize(sample.formula_latex).replace(/\$_{([^}]*)}\$/g, '$1 ');
+ content += "Sample in beam: " + formula + " at " + sample.density.toFixed(2) + " g/cm3 ";
+ if (density_str.indexOf(':') !== -1) content += " from lattice "+sanitize(density_str);
+ content += "
";
+
+ // Neutron scattering
+ content += '
\n ';
+ content += '1/e penetration depth (cm) ';
+ content += 'Scattering length density (10-6 /Å2 ) ';
+ content += 'Scattering cross section (1/cm) ';
+ content += 'X-ray SLD (10-6 /Å2 ) ';
+ content += ' \n ';
+ content += 'abs '+(1/scat.xs.abs).toFixed(3)+' ';
+ content += "real "+scat.sld.real.toFixed(3)+' ';
+ content += 'coh '+scat.xs.coh.toFixed(3)+' ';
+ content += "real "+xscat.sld.real.toFixed(3)+' ';
+ content += ' \n ';
+ content += 'abs+incoh '+(1/(scat.xs.incoh+scat.xs.abs)).toFixed(3)+' ';
+ content += "imag "+(-scat.sld.imag).toFixed(3)+' ';
+ content += 'abs '+scat.xs.abs.toFixed(3)+' ';
+ content += "imag "+(-xscat.sld.imag).toFixed(3)+' ';
+ content += ' \n ';
+ content += 'abs+incoh+coh '+scat.penetration.toFixed(3)+' ';
+ content += 'incoh '+scat.sld.incoh.toFixed(3)+' ';
+ content += 'incoh '+scat.xs.incoh.toFixed(3)+' ';
+ content += ''+''+' ';
+ content += ' \n ';
+ content += ' \n
';
+
+ // Neutron transmission
+ content += '
Neutron transmission is '
+ +(transmission>=2||transmitted_fluxTransmitted flux is '
+ +transmitted_flux.toPrecision(4)+' n/cm2 /s'
+ +' for a '+sanitize(flux_str)+' n/cm2 /s beam.\n';
+
+ // Contrast match
+ if (scat.contrast_match.D2O_fraction === null) {
+ content += "
";
+ } else {
+ content += '
Contrast match point: ';
+ if (scat.contrast_match.D2O_fraction < 0) {
+ content += '< 0% D
2 O\n';
+ } else if (scat.contrast_match.D2O_fraction > 1) {
+ content += '> 100% D
2 O\n';
+ } else {
+ console.log(scat.sld, scat);
+ content += (scat.contrast_match.D2O_fraction*100).toFixed(1)
+ + '% D
2 O by volume (real SLD = '
+ + scat.contrast_match.sld.toFixed(3)
+ + '×10
-6 /Å
2 )\n';
+ }
+ }
+ }
+
+
+ // End of div
+ content += '
\n';
+
+ var $new = $(content).prependTo('div[id=results]'); //add to the dom
+ // Start table unsorted so that b reactions follow transients as they
+ // do in the activation table. Note that there is no way to get back
+ // the original order.
+ $new.find("table").tablesorter();
+ scroll_to_result();
+}
+
+function scroll_to_result(){
+ var opt = {behavior: "smooth", block: "nearest"};
+ document.getElementById("results").firstChild.scrollIntoView(opt);
+}
+
+// Dynamic help support. Clicking an input field jumps to the
+// corresponding section of the help. Input "gork" uses:
+//
+// and the corresponding help section uses:
+//
Gork Section Title ...
+function help_jump(section_name){
+ var section = document.getElementById(section_name);
+ var frame = document.getElementById('help-frame');
+ if (!!section) {
+ frame.scrollTop = section.offsetTop; // - frame.offsetTop;
+ }
+}
+
+function setup_help() {
+ // $('#help-frame').height($('#activation-frame').height());
+ $('input').focusin(function(){help_jump("help_"+this.id.slice(3))});
+ $('.cite').click(function(event){
+ event.preventDefault();
+ help_jump(this.getAttribute('href').substring(1))
+ });
+}
+
+function ready_callback() {
+ document.querySelector("div.loader").classList.add("finished");
+}
+
+
+$(document).ready(function () {
+ API.initialize(process_response, ready_callback);
+ parse_url_parameters();
+ enable_forms();
+ setup_help();
+ // var timestamp = (new Date(Date.parse(document.lastModified))).toLocaleDateString();
+ // document.getElementById("lastmod-timestamp").innerHTML = timestamp;
+});
diff --git a/activation/css/footer.css b/activation/css/footer.css
new file mode 100644
index 0000000..807fb95
--- /dev/null
+++ b/activation/css/footer.css
@@ -0,0 +1,10 @@
+.footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem;
+}
+
+.footer .contacts {
+ flex: 1;
+}
diff --git a/activation/css/loading-spinner.css b/activation/css/loading-spinner.css
new file mode 100644
index 0000000..235b010
--- /dev/null
+++ b/activation/css/loading-spinner.css
@@ -0,0 +1,30 @@
+.loader .spinner {
+ border: 4px solid #f3f3f3; /* Light grey background */
+ border-top: 4px solid #3498db; /* Blue "spinning" part */
+ border-radius: 50%;
+ width: 30px;
+ height: 30px;
+ animation: spin 1s linear infinite;
+}
+
+.loader.finished {
+ display: none;
+}
+
+.loader {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 0 auto;
+ width: fit-content;
+ padding: 1em;
+}
+
+.loader.not-started {
+ display: none;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/activation/css/main.css b/activation/css/main.css
new file mode 100644
index 0000000..b139fea
--- /dev/null
+++ b/activation/css/main.css
@@ -0,0 +1,186 @@
+html, body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+@media print
+{
+ html, body { font-size: 10pt !important; height: unset; }
+ table, caption, th, td {
+ font-size: 10pt !important;
+ /* border: 0.1pt solid lightgray !important; */
+ border: none !important;
+ }
+ .no-print { display: none !important; height: 0; }
+ .no-print *{ display: none !important; height: 0; }
+ .print-only { display: block !important; }
+ .print-only *{ display: block !important; }
+}
+.print-only { display: none; }
+.print-only *{ display: none; }
+
+p { margin-bottom: 0; }
+.footer p { margin-top: 0; }
+
+/* body { font-size: 0.9em; } */
+.disabled { opacity: 0.3; }
+label { font-weight: inherit; }
+.controls input { width: 100%; }
+table { margin: 0; padding: 0; border-spacing: 0; border-color: lightgray; }
+caption, td, th { padding: 0 0.5em 0 0.5em; }
+th { vertical-align: baseline; }
+tbody tr:nth-child(2n) th, tbody tr.even th,
+tbody tr:nth-child(2n) td, tbody tr.even td,
+ul li:nth-child(2n), ol li:nth-child(2n) { background: inherit; /* background: none repeat scroll 0 0 #F3F3F9; */ }
+table.tablesorter thead tr .headerSortable {
+ padding-right: 2em; /* space for sort arrows */
+ cursor: pointer;
+}
+table.tablesorter thead tr .headerSortable::after {
+ content: " \25B4\25BE"; /* ▴▾ */
+ opacity: 0.4;
+}
+table.tablesorter thead tr .headerSortable.headerSortUp::after {
+ content: " \25B2"; /* ▲ */
+ opacity: 1;
+}
+table.tablesorter thead tr .headerSortable.headerSortDown::after {
+ content: " \25BC"; /* ▼ */
+ opacity: 1;
+}
+
+/* output markup */
+.error { color: red; }
+p.disclaimer { color: darkred; font-size:1.2em; }
+.activity_unusual { background-color:wheat; }
+/* .activity_normal { background-color: lightred; } */
+
+#id_chemical_formula { width: 30em; }
+
+div.cutoff { display: block; }
+div.nocutoff { display: none; }
+button.activity_button { float: right; }
+
+/* #wrapper {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+} */
+
+/* frame layout */
+#content-frame {
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+}
+
+#activation-frame {
+ width:30em;
+}
+
+#help-frame {
+ position: relative;
+ flex: 1;
+ /* min-width:10em; max-width:40em; */
+ overflow-y:scroll;
+}
+
+#help-wrapper {
+ position: absolute;
+ padding-left:1em;
+ padding-right: 1em;
+ margin-right:0em;
+}
+
+#help-wrapper .word-break {
+ word-break: break-all;
+}
+
+#results-frame {
+ overflow: auto;
+ flex: 1;
+ border-top:#005ea2 1px solid;
+ border-bottom:#005ea2 1px solid;
+}
+
+div.control-group { margin-left: 0.5em; vertical-align: bottom; }
+/* div.control-group { height: 3ex; width: 15em; } */
+/* .help starts display:none so that size is initially zero. */
+.help { display:block; }
+/*.help dl { margin-left: 30em; }
+.help ol { margin-left: 30em; }
+.help p { margin-left: 30em; }
+*/
+.help h3 { margin-top: 0.25em; }
+.help dd { margin-left: 1em; }
+.help dt { margin-left: 0em; padding: 0.3em; font-weight: bold; }
+.help ol { list-style-position: inside; padding-left: 0em; }
+.help li { margin-left: 0em; margin-bottom: 1ex; }
+.help div.bold { font-weight: bold; display: inline; }
+.help div.entrytext {
+ display:inline;
+ font-weight:bold;
+ font-style:oblique;
+}
+.help div.example:before { content:"Example: "; }
+
+/* fading horizontal rule
+modified from http://konigi.com/tools/css-techniques-horizontal-rules
+*/
+.help hr {
+ width: 100%;
+ height: 1px;
+ margin: 2.4em 0;
+ border: none;
+ background: #ddd;
+ background-image: -webkit-gradient(
+ linear,
+ left bottom,
+ right bottom,
+ color-stop(0, rgb(255,255,255)),
+ color-stop(0.1, rgb(221,221,221)),
+ color-stop(0.9, rgb(221,221,221)),
+ color-stop(1, rgb(255,255,255))
+ );
+ background-image: -moz-linear-gradient(
+ left center,
+ rgb(255,255,255) 0%,
+ rgb(221,221,221) 10%,
+ rgb(221,221,221) 90%,
+ rgb(255,255,255) 100%
+ );
+}
+
+/* dictionary type: value on same line */
+dl.inline {
+ display: -ms-grid; -ms-grid-template-columns: max-content auto;
+ display: grid; grid-template-columns: max-content auto;
+}
+dl.inline dt {
+ -ms-grid-column-start: 1;
+ grid-column-start: 1;
+ padding: 0.1em;
+}
+dl.inline dd {
+ -ms-grid-column-start: 2;
+ grid-column-start: 2;
+ padding: 0.1em; margin-left: 0.5em;
+}
+
+.panel { border-radius:0.6em; border: 2pt solid gray; margin: 0.5em; padding: 0.5em; }
+.panel h3 { margin-top: -1em; margin-bottom: 0.3em; font-size: 1em; }
+.panel h3 .text { background: white; margin-left: 1em; padding: 0 0.3em 0 0.3em; }
+.panel { position: relative; }
+.panel .btn { position:absolute; right: 1em; top: 0.75em;}
+/* .panel .btn { position:absolute; top:1em; right:1em; } */
+
+/* Keep constant baseline spacing even with superscript/subscript */
+sup, sub {
+ height: 0;
+ line-height: 1;
+ vertical-align: baseline;
+ _vertical-align: bottom;
+ position: relative;
+}
+sup { bottom: 1ex; }
+sub { top: .5ex; }
diff --git a/activation/css/nist-header.css b/activation/css/nist-header.css
new file mode 100644
index 0000000..d75f88d
--- /dev/null
+++ b/activation/css/nist-header.css
@@ -0,0 +1,56 @@
+@font-face {
+ font-family:"Source Sans Pro Web";
+ font-style:normal;
+ font-weight:400;
+ font-display:fallback;
+ src: url(https://www.nist.gov/libraries/nist-component-library/dist/fonts/source-sans-pro/sourcesanspro-bold-webfont.woff2) format("woff2"),
+ url(https://www.nist.gov/libraries/nist-component-library/dist/fonts/source-sans-pro/sourcesanspro-regular-webfont.woff) format("woff"),
+ url(https://www.nist.gov/libraries/nist-component-library/dist/fonts/source-sans-pro/sourcesanspro-regular-webfont.ttf) format("truetype");
+}
+
+.ncnr-nist-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #ddd;
+ background: url(https://www.nist.gov/libraries/nist-component-library/dist/img/pattern/bg_pattern.png) #006dbc;
+ position: relative;
+}
+
+.ncnr-nist-header::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ background-image: linear-gradient(to right,transparent,#005ea2);
+}
+
+.ncnr-nist-header h2 {
+ margin: 0;
+ font-family: Source Sans Pro Web;
+ font-size: 1.86rem;
+ line-height: 1.1;
+ text-decoration: none;
+ padding-left: 1rem;
+ text-transform: uppercase;
+ font-weight: 700;
+}
+
+.ncnr-nist-header a {
+ text-decoration: none;
+ color: #FFF;
+ z-index: 2;
+ position: relative;
+}
+
+.ncnr-nist-header img {
+ position: relative;
+ padding-right: 2rem;
+ max-width: 100px;
+ height: auto;
+ z-index: 2;
+}
diff --git a/activation/get_pyodide.sh b/activation/get_pyodide.sh
new file mode 100755
index 0000000..dc9e539
--- /dev/null
+++ b/activation/get_pyodide.sh
@@ -0,0 +1,30 @@
+# !/bin/bash
+
+# Run in the activation directory (where this script lives)
+cd "$(dirname "$0")"
+
+# Default version of pyodide to download if not set via environment variable
+PYODIDE_VERSION=${PYODIDE_VERSION:-0.29.3}
+
+rm -rf pyodide
+mkdir pyodide
+
+# Get the core pyodide files first (this is smaller and faster to download than the full version)
+curl -L https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-core-${PYODIDE_VERSION}.tar.bz2 -o pyodide.tar.bz2
+ tar -xjf pyodide.tar.bz2 -C ./pyodide --strip-components=1
+
+# Download the full version to get the specific wheels we need (numpy, pytz, micropip)
+curl -L https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-${PYODIDE_VERSION}.tar.bz2 -o pyodide_full.tar.bz2
+
+# Extract only the core runtime + our specific wheels
+TAR_OPTIONS="--wildcards" tar -xjf pyodide_full.tar.bz2 "*micropip-*.whl" "*numpy-*.whl" "*pytz-*.whl" "*pyparsing-*.whl"
+
+# Download the latest periodictable wheel from PyPI
+pip3 download periodictable --no-deps --only-binary :all: -d ./pyodide/
+
+# Write out the full wheel file name to a text file for use in the workflow
+ls pyodide/periodictable-*.whl > periodictable_wheel_name.txt
+
+# Cleanup
+rm -f pyodide.tar.bz2
+rm -f pyodide_full.tar.bz2
diff --git a/activation/index.html b/activation/index_template.html
similarity index 52%
rename from activation/index.html
rename to activation/index_template.html
index 0a1bfea..b661a5d 100644
--- a/activation/index.html
+++ b/activation/index_template.html
@@ -4,733 +4,17 @@
Neutron Activation and Scattering Calculator
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
@@ -760,7 +44,7 @@ Material
@@ -789,7 +73,7 @@ Neutron Activation
-
+
@@ -800,7 +84,7 @@ Neutron Activation
Mass
- Exposure
+ Exposure
Decay
@@ -811,7 +95,7 @@ Neutron Activation
-
+
@@ -1172,7 +456,7 @@ Material mass
-
+
Exposure
Units: h m s d w y
@@ -1453,6 +737,7 @@
History
Activation and Scattering Results
+
Initializing calculator...
@@ -1464,7 +749,7 @@
Activation and Scattering Results
-
Using
periodictable v2.0.0
diff --git a/activation/webworker.js b/activation/webworker.js
new file mode 100644
index 0000000..b26812c
--- /dev/null
+++ b/activation/webworker.js
@@ -0,0 +1,60 @@
+// webworker.js
+
+import { loadPyodide } from "./pyodide/pyodide.mjs";
+// import { loadPyodide } from "./pyodide/pyodide.mjs";
+
+async function loadPyodideAndPackages() {
+ self.pyodide = await loadPyodide();
+ await self.pyodide.loadPackage(["numpy", "pytz", "micropip"]);
+
+ // get the periodictable wheel name from the special file:
+ const response = await fetch("./periodictable_wheel_name.txt");
+ const wheelName = (await response.text()).trim();
+ await self.pyodide.runPythonAsync(`
+ import micropip
+ await micropip.install("./${wheelName}")
+ import periodictable
+ print(periodictable.__version__)
+ `)
+
+ // Downloading a single file
+ await pyodide.runPythonAsync(`
+ from pyodide.http import pyfetch
+ response = await pyfetch("./nact.py")
+ with open("nact.py", "wb") as f:
+ f.write(await response.bytes())
+ import nact
+ import json
+
+ `)
+}
+let pyodideReadyPromise = loadPyodideAndPackages();
+pyodideReadyPromise.then(() => self.postMessage({worker_ready: true}));
+
+self.onmessage = async (event) => {
+ // make sure loading is done
+ await pyodideReadyPromise;
+ // Now is the easy part, the one that is similar to working in the main thread:
+ try {
+ const json_data = JSON.stringify(event.data.data);
+ let python = `
+ request = json.loads('${json_data}')
+ try:
+ response = nact.api_call(request)
+ except Exception:
+ response = {
+ 'success': False,
+ 'version': periodictable.__version__,
+ 'detail': {'query': error()},
+ 'error': 'unexpected exception',
+ }
+ json.dumps(response)
+ `;
+ //console.log('python:', python);
+ let results = await self.pyodide.runPythonAsync(python);
+ let ldata = JSON.parse(results);
+ self.postMessage(ldata);
+ } catch (error) {
+ self.postMessage({ success: false, detail: {error: error.message }});
+ }
+};
\ No newline at end of file
diff --git a/cgi-bin/nact.py b/cgi-bin/nact.py
index 24cc2f8..2c9d821 100755
--- a/cgi-bin/nact.py
+++ b/cgi-bin/nact.py
@@ -8,7 +8,6 @@
from __future__ import print_function
import sys
-import cgi
import re
import json
from math import exp
@@ -16,11 +15,7 @@
from datetime import datetime, timedelta
from calendar import monthrange
-# CRUFT: python 2 doesn't have html.escape
-try:
- from html import escape
-except ImportError:
- from cgi import escape
+from html import escape
from pytz import timezone, utc
@@ -253,41 +248,38 @@ def parse_date(datestring, default_timezone=default_timezone):
dt = utc.localize(dt) - timedelta(0, offset)
return dt
-def cgi_call():
- form = cgi.FieldStorage()
- #print(form, file=sys.stderr)
- #print >>sys.stderr, "sample",form.getfirst('sample')
- #print >>sys.stderr, "mass",form.getfirst('mass')
-
+def api_call(form: dict):
# Parse inputs
errors = {}
- calculate = form.getfirst('calculate', 'all')
+ calculate = form.get('calculate', 'all')
if calculate not in ('scattering', 'activation', 'all'):
errors['calculate'] = "calculate should be one of 'scattering', 'activation' or 'all'"
try:
- sample = form.getfirst('sample')
+ sample = form.get('sample')
chem = formula(sample)
except Exception:
errors['sample'] = error()
try:
- fluence = float(form.getfirst('flux', 100000))
+ fluence = float(form.get('flux', 100000))
except Exception:
errors['flux'] = error()
try:
- fast_ratio = float(form.getfirst('fast', '0'))
+ fast_ratio = float(form.get('fast', '0'))
except Exception:
errors['fast'] = error()
try:
- Cd_ratio = float(form.getfirst('Cd', '0'))
+ Cd_ratio = float(form.get('Cd', '0'))
except Exception:
errors['Cd'] = error()
try:
- exposure = parse_hours(form.getfirst('exposure', '1'))
+ exposure = parse_hours(form.get('exposure', '1'))
except Exception:
errors['exposure'] = error()
try:
- mass_str = form.getfirst('mass', '0')
- if mass_str.endswith('kg'):
+ mass_str = form.get('mass', '0')
+ if not mass_str:
+ mass = 0
+ elif mass_str.endswith('kg'):
mass = 1000*float(mass_str[:-2])
elif mass_str.endswith('mg'):
mass = 0.001*float(mass_str[:-2])
@@ -300,26 +292,26 @@ def cgi_call():
except Exception:
errors['mass'] = error()
try:
- density_type, density_value = parse_density(form.getfirst('density', '0'))
+ density_type, density_value = parse_density(form.get('density', '0'))
except Exception:
errors['density'] = error()
try:
- #print >>sys.stderr,form.getlist('rest[]')
- rest_times = [parse_rest(v) for v in form.getlist('rest[]')]
+ rest_times = [parse_rest(v) for v in form.get('rest', [])]
if not rest_times:
rest_times = [0, 1, 24, 360]
except Exception:
errors['rest'] = error()
try:
- decay_level = float(form.getfirst('decay', '0.001'))
+ decay_level = float(form.get('decay', '0.001'))
except Exception:
errors['decay'] = error()
try:
- thickness = float(form.getfirst('thickness', '1'))
+ thickness = float(form.get('thickness', '1'))
except Exception:
errors['thickness'] = error()
try:
- wavelength_str = form.getfirst('wavelength', '1').strip()
+ # Ensure we cast to string so we can safely .strip()
+ wavelength_str = str(form.get('wavelength', '1')).strip()
if wavelength_str.endswith('meV'):
wavelength = nsf.neutron_wavelength(float(wavelength_str[:-3]))
elif wavelength_str.endswith('m/s'):
@@ -328,11 +320,10 @@ def cgi_call():
wavelength = float(wavelength_str[:-3])
else:
wavelength = float(wavelength_str)
- #print >>sys.stderr,wavelength_str
except Exception:
errors['wavelength'] = error()
try:
- xray_source = form.getfirst('xray', 'Cu Ka').strip()
+ xray_source = str(form.get('xray', 'Cu Ka')).strip()
if xray_source.endswith('Ka'):
xray_wavelength = elements.symbol(xray_source[:-2].strip()).K_alpha
elif xray_source.endswith('keV'):
@@ -343,14 +334,12 @@ def cgi_call():
xray_wavelength = elements.symbol(xray_source).K_alpha
else:
xray_wavelength = float(xray_source)
- #print >>sys.stderr,"xray",xray_source,xray_wavelength
except Exception:
errors['xray'] = error()
try:
- abundance_source = form.getfirst('abundance', 'IAEA')
+ abundance_source = form.get('abundance', 'IAEA')
if abundance_source == "IUPAC":
abundance = activation.table_abundance
- # CRUFT: periodictable no longer uses NIST 2001 data for abundance
elif abundance_source == "NIST":
abundance = activation.table_abundance
elif abundance_source == "IAEA":
@@ -490,10 +479,33 @@ def cgi_call():
return result
+def fieldstorage_to_dict(form: 'cgi.FieldStorage') -> dict:
+ """
+ special handling for "rest", which is a list of times.
+ """
+ result = {}
+ if form is None:
+ return result
+
+ for key in form.keys():
+ values = form.getlist(key)
+ if len(values) == 1:
+ value = values[0]
+ if key == "rest":
+ result[key] = value.split(",")
+ else:
+ result[key] = value
+
+ return result
if __name__ == "__main__":
+ import cgi
try:
- response = cgi_call()
+ form = cgi.FieldStorage()
+
+ # Convert to dictionary
+ form_dict = fieldstorage_to_dict(form)
+ response = api_call(form_dict)
except Exception:
response = {
'success': False,
diff --git a/deploy_calculator.sh b/deploy_calculator.sh
new file mode 100755
index 0000000..4b97ea3
--- /dev/null
+++ b/deploy_calculator.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+TARGET_DIR=${TARGET_DIR:-/var/www/html/resources/activation}
+
+# First, get the latest pyodide files and the specific periodictable wheel we need:
+./activation/get_pyodide.sh
+
+# Now copy all the necessary files to the target directory for deployment:
+mkdir -p $TARGET_DIR
+cp activation/jquery* $TARGET_DIR/
+cp activation/*.js $TARGET_DIR/
+cp activation/webworker.js $TARGET_DIR/
+cp activation/favicon.ico $TARGET_DIR/
+cp activation/periodictable_wheel_name.txt $TARGET_DIR/
+cp cgi-bin/nact.py $TARGET_DIR/
+cp -r activation/pyodide $TARGET_DIR/pyodide
+cp -r activation/css $TARGET_DIR/
+
+# Get the version of periodictable from the wheel file name and write it to a text file for use in the workflow
+PERIODICTABLE_VERSION=$(python -c "import periodictable; print(periodictable.__version__)")
+
+# Write replacements in template
+API_SUB="s@{{ api_script }}@api_webworker.js@g"
+VER_SUB="s@{{ periodictable_version }}@$PERIODICTABLE_VERSION@g"
+sed -e "$API_SUB;$VER_SUB" activation/index_template.html > "$TARGET_DIR/index.html"
diff --git a/flask_server.py b/flask_server.py
new file mode 100644
index 0000000..5710ed3
--- /dev/null
+++ b/flask_server.py
@@ -0,0 +1,70 @@
+from pathlib import Path
+import re
+import sys
+from flask import abort, Flask, request, jsonify, make_response, render_template, send_from_directory
+
+# Import your refactored script (ensure it's named nact.py in the same folder)
+sys.path.append(str(Path(__file__).parent / 'cgi-bin'))
+import nact
+
+app = Flask(__name__, template_folder="activation")
+
+
+@app.route('/api/calculate', methods=['POST'])
+def calculate():
+ """
+ Handles the API request, converts the input into a standard dict,
+ and passes it to nact.cgi_call().
+ """
+
+ try:
+ data = request.get_json()
+ result = nact.api_call(data)
+ return jsonify(result)
+ except Exception as e:
+ return jsonify({
+ 'success': False,
+ 'error': 'Server error',
+ 'detail': str(e)
+ }), 500
+
+
+@app.route('/')
+@app.route('/index.html')
+def serve_index():
+ """
+ Reads the index.html file, performs a regex substitution on a script tag,
+ and returns the modified HTML.
+ """
+ return render_template(
+ 'index_template.html',
+ api_script='api_fetch.js',
+ periodictable_version=nact.periodictable.__version__
+ )
+
+
+@app.route('/')
+def serve_static_files(filename):
+ """
+ Serves any other file requested at the root level from the 'activation' folder.
+ """
+ # Prevent this route from accidentally serving the unmodified index.html
+ if filename == 'index.html':
+ # You can either call serve_index() directly or return a 404
+ return serve_index()
+
+ # Securely send the file from the 'activation' directory
+ try:
+ return send_from_directory('activation', filename)
+ except FileNotFoundError:
+ abort(404)
+
+
+if __name__ == '__main__':
+ if len(sys.argv) > 1:
+ host, *rest = sys.argv[1].split(':', 1)
+ port = int(rest[0]) if rest else 8008
+ else:
+ host, port = "localhost", 8008
+ # Run the server in debug mode on port 5000
+ app.run(debug=True, host=host, port=port)
\ No newline at end of file
diff --git a/server.py b/server.py
deleted file mode 100755
index 234575a..0000000
--- a/server.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python
-"""
-Simple server for running the neutron activation calculator.
-
-This is intended for testing. It has not been assessed for security and is
-not recommended for a public facing server. The activation calculator
-expects a cgi interface, which should be provided by the web infrastructure
-(apache, nginx, etc.) that you are using on your production server.
-
-Usage: python server.py [host | host:port]
-
-Default is localhost:8008
-"""
-
-from __future__ import print_function
-
-import sys
-import os
-try:
- from http.server import HTTPServer, CGIHTTPRequestHandler
- from socketserver import ThreadingMixIn
-except ImportError:
- from BaseHTTPServer import HTTPServer
- from SocketServer import ThreadingMixIn
- from CGIHTTPServer import CGIHTTPRequestHandler
-import cgitb
-cgitb.enable() ## This line enables CGI error reporting
-
-class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
- """Very simple threaded server"""
- allow_reuse_address = True
- request_queue_size = 50
-
-server = ThreadedHTTPServer
-handler = CGIHTTPRequestHandler
-handler.cgi_directories = ["/cgi-bin"]
-
-if len(sys.argv) > 1:
- host, *rest = sys.argv[1].split(':', 1)
- port = int(rest[0]) if rest else 8008
-else:
- host, port = "", 8008
-print(f"serving on http://{host}:{port}/activation/")
-httpd = server((host, port), handler)
-httpd.serve_forever()