見出し画像

ChatGPTに訊いてみた:分子量計算アプリを作りたい!(第11回)~カスタム官能基・芳香環ボタンの追加・削除と、原子量へのこだわり(ver.7.7)~



Q. 所長

前回の分二郎 ver.7.6 では、官能基ボタン・芳香環ボタン・–Hボタンを実装して、かなり実用的になりました。
ですが、研究室によって「オリジナルの官能基」や「特定の芳香環誘導体」をよく使うこともあります。
できれば、それらを自由に追加・削除できるようにしたいんですよ。

それともうひとつ気になるのが、分子量計算に使う原子量です。
IUPACと日本化学会で値が微妙に違うと聞きますが、どちらを使っているんですか?


A. ChatGPT

いい質問ですね、所長。今回 ver.7.7 での改良と、原子量周りの戦略をまとめてお話します。

🚀 機能面の改良ポイント

  • 官能基/芳香環ボタンを、ユーザー自身で追加・削除できるように

  • 削除時には確認ダイアログを出し、誤操作を防止

  • 追加・削除情報は functional_groups.json および aromatic_rings.json に保存

  • –H ボタンによる水素除去や、これまでのベタ打ち/周期表入力を引き続きサポート

  • UIを整理し、見た目も使い勝手も向上

  • atomic_weights.csv → atomic_weights.json の変換機能をアプリ内に組み込み


🔬 原子量へのこだわり:中央値採用という選択

原子量(標準原子量)は、IUPAC や日本化学会で最新化が進んでおり、多くの元素に 範囲(interval)表記 が導入されています。
これは、自然界での 同位体比変動 を反映した考え方です。

しかし、実用アプリで分子量を使う際は、範囲そのものを扱うのではなく「代表値」を使うことが一般的です。
そこで本アプリでは、範囲をもつ元素は中央値を採用する方針を採りました。

🧩 補足解説:中央値 vs 標準値の考え方

  • 一部の元素(例:C, N, O など)は変動幅が極めて小さいため、従来通り 単一の標準値 として扱われます。
    → 例:炭素は [12.0096, 12.0116] の範囲がありますが、教育・実務上は 12.011 を固定値とします。

  • 一方、硫黄 (S) や塩素 (Cl) のように同位体比変動が比較的大きい元素では、
    標準原子量範囲の中央値 を代表値として用います。

⚛️ 例えば:

標準原子量の範囲と採用値

このように、範囲を中央値へ変換し、さらに 有効数字4桁(小数第3位) に丸めて採用しています。


📄 atomic_weights.csv(サンプル)

Symbol,Element,AtomicWeight
H,Hydrogen,1.008
C,Carbon,12.011
N,Nitrogen,14.007
O,Oxygen,15.999
S,Sulfur,32.067
Na,Sodium,22.990
Cl,Chlorine,35.452
Fe,Iron,55.845
Cu,Copper,63.546
Zn,Zinc,65.38

この CSV をもとに、Flask 内の変換機能で atomic_weights.json を生成します。


📁 ファイル構成

myProject/
 ├─ app.py
 ├─ templates/
 │   └─ index.html
 └─ public/
     ├─ atomic_weights.csv
     ├─ functional_groups.json
     ├─ aromatic_rings.json
     └─ atomic_weights.json

💻 Flaskでの更新機能追加

今回のver.7.7では、原子量表を管理・再生成できるように
Flaskアプリに管理用の手動変換機能(/convert_csv)を追加しました。

これにより、atomic_weights.csv を修正して保存すれば、
ブラウザ上から /convert_csv にアクセスするだけで
atomic_weights.json を再生成できます。


🔧 app.py(主要部抜粋・説明)

# --- CSV→JSON変換関数 ---
def convert_csv_to_json(csv_path="atomic_weights.csv", json_path="atomic_weights.json"):
    """atomic_weights.csv を atomic_weights.json に変換する"""
    ...
# --- 管理者用エンドポイント ---
@app.route("/convert_csv", methods=["GET"])
def convert_csv_endpoint():
    """atomic_weights.csv → atomic_weights.json の手動変換"""
    result = convert_csv_to_json()
    return jsonify(result)

➡ これにより、ファイル更新が容易になります。
アプリを再起動せずに新しい原子量データを反映可能です。


🐍 app.py

# -*- coding: utf-8 -*-
# app.py (ver.7.7)
from flask import Flask, request, jsonify, render_template
import json, re, os, webbrowser, csv
from threading import Timer

# --- ディレクトリ構成 ---
APP_DIR = os.path.dirname(os.path.abspath(__file__))
PUBLIC_DIR = os.path.join(APP_DIR, "public")
CSV_PATH = os.path.join(PUBLIC_DIR, "atomic_weights.csv")
JSON_PATH = os.path.join(PUBLIC_DIR, "atomic_weights.json")

app = Flask(__name__, template_folder="templates", static_folder=PUBLIC_DIR)

# --- CSV→JSON変換関数 ---
def convert_csv_to_json(csv_path=CSV_PATH, json_path=JSON_PATH):
    """atomic_weights.csv を atomic_weights.json に変換する"""
    if not os.path.exists(csv_path):
        return {"status": "error", "message": f"{csv_path} が見つかりません。"}

    data = []
    with open(csv_path, encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        for row in reader:
            if not row.get("Symbol") or not row.get("AtomicWeight"):
                continue
            try:
                aw = float(row["AtomicWeight"])
            except ValueError:
                aw = None
            data.append({
                "symbol": row["Symbol"].strip(),
                "name": row["Element"].strip().capitalize(),
                "atomic_mass": aw
            })

    with open(json_path, "w", encoding="utf-8") as jf:
        json.dump(data, jf, indent=2, ensure_ascii=False)

    return {"status": "success", "message": f"{os.path.basename(json_path)} を再生成しました。", "count": len(data)}


@app.route("/convert_csv", methods=["GET"])
def convert_csv_endpoint():
    """管理者用:atomic_weights.csv → atomic_weights.json の手動変換"""
    result = convert_csv_to_json()
    return jsonify(result)

# --- JSON 読み込み/保存 ---
def load_json(filename):
    path = os.path.join(PUBLIC_DIR, filename)
    if not os.path.exists(path):
        return []
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

def save_json(filename, data):
    with open(os.path.join(PUBLIC_DIR, filename), "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

# --- データ読み込み ---
elements = load_json("atomic_weights.json")
functional_groups = load_json("functional_groups.json")
aromatic_rings = load_json("aromatic_rings.json")

# --- 原子量辞書作成 ---
atomic_weights = {e["symbol"]: e["atomic_mass"] for e in elements}
atomic_weights.update({g["symbol"]: g["atomic_mass"] for g in functional_groups})
atomic_weights.update({a["symbol"]: a["atomic_mass"] for a in aromatic_rings})

TOK_RE = re.compile(r"([A-Z][a-z]?)(\d*)")

def calculate_mw(formula: str) -> float:
    mw, pos = 0.0, 0
    for m in TOK_RE.finditer(formula):
        el, num = m.group(1), m.group(2)
        if el not in atomic_weights:
            raise ValueError(f"未知の元素: {el}")
        count = int(num) if num else 1
        mw += atomic_weights[el] * count
        pos = m.end()
    if pos != len(formula):
        raise ValueError("不正な文字が含まれています")
    return mw

@app.route("/")
def index():
    return render_template("index.html",
                           functional_groups=functional_groups,
                           aromatic_rings=aromatic_rings)

@app.route("/calculate", methods=["POST"])
def api_calculate():
    data = request.get_json()
    try:
        mw = calculate_mw(data.get("formula", ""))
        return jsonify({"formula": data["formula"], "mw": mw})
    except Exception as e:
        return jsonify({"error": str(e)}), 400

@app.route("/add_group", methods=["POST"])
def add_group():
    data = request.get_json()
    entry = data["entry"]
    if data["type"] == "fg":
        functional_groups.append(entry)
        save_json("functional_groups.json", functional_groups)
    else:
        aromatic_rings.append(entry)
        save_json("aromatic_rings.json", aromatic_rings)
    return jsonify({"status": "ok"})

@app.route("/delete_group", methods=["POST"])
def delete_group():
    data = request.get_json()
    target = data["symbol"]
    if data["type"] == "fg":
        functional_groups[:] = [g for g in functional_groups if g["symbol"] != target]
        save_json("functional_groups.json", functional_groups)
    else:
        aromatic_rings[:] = [r for r in aromatic_rings if r["symbol"] != target]
        save_json("aromatic_rings.json", aromatic_rings)
    return jsonify({"status": "deleted"})

def open_browser():
    webbrowser.open("http://127.0.0.1:5000/")

if __name__ == "__main__":
    Timer(1, open_browser).start()
    app.run(host="127.0.0.1", port=5000, debug=True)

🌐 index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>分二郎 ver.7.7</title>
    <style>
        body {
            font-family: "Segoe UI", sans-serif;
            text-align: center;
            background-color: #fafafa;
        }

        h1 {
            margin: 20px;
            font-size: 2em;
            font-weight: bold;
        }

        .tabs {
            display: flex;
            justify-content: center;
            margin-bottom: 15px;
        }

        .tab {
            padding: 10px 20px;
            margin: 0 5px;
            border: 1px solid #999;
            border-radius: 6px;
            background-color: #eee;
            cursor: pointer;
        }

            .tab.active {
                background-color: #ddd;
                font-weight: bold;
            }

        .panel {
            display: none;
            margin: 0 auto;
        }

            .panel.active {
                display: block;
            }

        .input-box {
            width: 300px;
            padding: 8px;
            font-size: 18px;
            margin-bottom: 10px;
        }

        .button-grid {
            display: grid;
            grid-template-columns: repeat(18, 40px);
            gap: 4px;
            justify-content: center;
            margin-top: 10px;
        }

        .btn {
            padding: 6px;
            border: 1px solid #555;
            border-radius: 5px;
            background-color: #f0f0f0;
            cursor: pointer;
        }

            .btn:hover {
                background-color: #ddd;
            }

        /* --- 周期表色分け --- */
        .alkali {
            background-color: #ffcccc;
        }
        /* アルカリ金属 */
        .alkaline {
            background-color: #ffebcc;
        }
        /* アルカリ土類金属 */
        .transition {
            background-color: #cce0ff;
        }
        /* 遷移金属 */
        .post-transition {
            background-color: #e6e6e6;
        }
        /* 後遷移金属 */
        .metalloid {
            background-color: #e0f2e0;
        }
        /* 半金属 */
        .nonmetal {
            background-color: #ffffcc;
        }
        /* 非金属 */
        .halogen {
            background-color: #ffe0cc;
        }
        /* ハロゲン */
        .noble {
            background-color: #e6ccff;
        }
        /* 希ガス */
        .lanthanoid {
            background-color: #ffccff;
        }
        /* ランタノイド */
        .actinoid {
            background-color: #ffb3b3;
        }
        /* アクチノイド */

        .num-grid {
            display: grid;
            grid-template-columns: repeat(10, 40px) 50px 50px 50px;
            gap: 5px;
            justify-content: center;
            margin-top: 10px;
        }

        .num-btn {
            background-color: #fce4ec;
            border: 1px solid #666;
            border-radius: 5px;
            cursor: pointer;
            padding: 6px;
        }

            .num-btn:hover {
                background-color: #f8bbd0;
            }

        .special-btn {
            background-color: #ffcdd2;
            font-weight: bold;
        }

        .delete-btn {
            color: red;
            font-weight: bold;
            margin-left: 4px;
            cursor: pointer;
        }

        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.4);
            justify-content: center;
            align-items: center;
        }

        .modal-content {
            background: white;
            padding: 20px;
            border-radius: 10px;
            width: 300px;
        }
    </style>
</head>
<body>
    <h1>分二郎 ver.7.7</h1>

    <div class="tabs">
        <div class="tab active" data-tab="manualTab">ベタ打ち入力</div>
        <div class="tab" data-tab="periodicTab">周期表入力</div>
        <div class="tab" data-tab="fgTab">官能基ボタン</div>
        <div class="tab" data-tab="aromaticTab">芳香環ボタン</div>
    </div>

    <input id="formulaInput" class="input-box" type="text" placeholder="化学式を入力">
    <div id="result">ここに結果が表示されます</div>

    <!-- タブパネル -->
    <div id="manualTab" class="panel active">
        <p>化学式を直接入力してください。</p>
    </div>

    <div id="periodicTab" class="panel">
        <div class="button-grid" id="periodicTable"></div>
    </div>

    <div id="fgTab" class="panel">
        <div id="fgButtons"></div>
        <button onclick="openModal('fg')">+追加</button>
    </div>

    <div id="aromaticTab" class="panel">
        <div id="aromaticButtons"></div>
        <button onclick="openModal('aromatic')">+追加</button>
    </div>

    <!-- モーダル -->
    <div id="modal" class="modal">
        <div class="modal-content">
            <h3>新しいボタンを追加</h3>
            <p><input id="symbol" placeholder="Symbol (例: SO3H)" style="width:90%;"></p>
            <p><input id="name" placeholder="Name (例: Sulfonic acid)" style="width:90%;"></p>
            <p><input id="formula" placeholder="Formula (例: SO3H)" style="width:90%;"></p>
            <button onclick="addItem()">追加</button>
            <button onclick="closeModal()">キャンセル</button>
        </div>
    </div>

    <!-- 数字・AC・BS・-H -->
    <div class="num-grid">
        <button class="num-btn">1</button><button class="num-btn">2</button><button class="num-btn">3</button>
        <button class="num-btn">4</button><button class="num-btn">5</button><button class="num-btn">6</button>
        <button class="num-btn">7</button><button class="num-btn">8</button><button class="num-btn">9</button>
        <button class="num-btn">0</button>
        <button class="num-btn special-btn" id="acBtn">AC</button>
        <button class="num-btn special-btn" id="bsBtn">BS</button>
        <button class="num-btn special-btn" id="minusHBtn">-H</button>
    </div>

    <script>
        // --- 初期データ ---
        let fgList = [
            { symbol: "OH", name: "Hydroxyl", formula: "OH" },
            { symbol: "COOH", name: "Carboxyl", formula: "COOH" },
            { symbol: "NH2", name: "Amino", formula: "NH2" },
            { symbol: "NO2", name: "Nitro", formula: "NO2" }
        ];
        let aromaticList = [
            { symbol: "C6H6", name: "Benzene", formula: "C6H6" },
            { symbol: "C10H8", name: "Naphthalene", formula: "C10H8" },
            { symbol: "C5H5N", name: "Pyridine", formula: "C5H5N" }
        ];
        let currentType = "";

        // --- ボタン描画 ---
        function renderButtons() {
            const fgDiv = document.getElementById("fgButtons");
            fgDiv.innerHTML = "";
            fgList.forEach((g, i) => {
                const btn = document.createElement("button");
                btn.className = "btn";
                btn.textContent = g.symbol;
                btn.onclick = () => appendFormula(g.formula);
                const del = document.createElement("span");
                del.className = "delete-btn";
                del.textContent = "🗑";
                del.onclick = () => {
                    if (confirm(`${g.symbol} を削除しますか?`)) {
                        fgList.splice(i, 1); renderButtons();
                    }
                };
                fgDiv.appendChild(btn);
                fgDiv.appendChild(del);
            });

            const arDiv = document.getElementById("aromaticButtons");
            arDiv.innerHTML = "";
            aromaticList.forEach((a, i) => {
                const btn = document.createElement("button");
                btn.className = "btn";
                btn.textContent = a.symbol;
                btn.onclick = () => appendFormula(a.formula);
                const del = document.createElement("span");
                del.className = "delete-btn";
                del.textContent = "🗑";
                del.onclick = () => {
                    if (confirm(`${a.symbol} を削除しますか?`)) {
                        aromaticList.splice(i, 1); renderButtons();
                    }
                };
                arDiv.appendChild(btn);
                arDiv.appendChild(del);
            });
        }
        renderButtons();

        // --- タブ切り替え ---
        document.querySelectorAll(".tab").forEach(tab => {
            tab.addEventListener("click", () => {
                document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
                document.querySelectorAll(".panel").forEach(p => p.classList.remove("active"));
                tab.classList.add("active");
                document.getElementById(tab.dataset.tab).classList.add("active");
            });
        });

        function appendFormula(str) {
            const input = document.getElementById("formulaInput");
            input.value += str;
            calculate(input.value);
        }

        async function calculate(formula) {
            const res = await fetch("/calculate", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ formula })
            });
            const data = await res.json();
            document.getElementById("result").textContent = res.ok ? `${data.formula} の分子量 = ${data.mw.toFixed(3)} g/mol` : "エラー: " + data.error;
        }

        // --- モーダル ---
        function openModal(type) { currentType = type; document.getElementById("modal").style.display = "flex"; }
        function closeModal() { document.getElementById("modal").style.display = "none"; }

        function addItem() {
            const s = document.getElementById("symbol").value.trim();
            const n = document.getElementById("name").value.trim();
            const f = document.getElementById("formula").value.trim();
            if (!s || !f) { alert("SymbolとFormulaは必須です"); return; }
            if (currentType === "fg") fgList.push({ symbol: s, name: n, formula: f });
            if (currentType === "aromatic") aromaticList.push({ symbol: s, name: n, formula: f });
            renderButtons();
            closeModal();
            document.getElementById("symbol").value = "";
            document.getElementById("name").value = "";
            document.getElementById("formula").value = "";
        }

        // --- 数字/AC/BS/-H ボタン ---
        const formulaInput = document.getElementById("formulaInput");

        // ★ 追加:ベタ打ち入力にも反応するようにする
        formulaInput.addEventListener("input", () => calculate(formulaInput.value));

        document.querySelectorAll(".num-btn").forEach(btn => {
            btn.addEventListener("click", () => {
                const val = btn.textContent;
                if (val === "AC") {
                    formulaInput.value = "";
                } else if (val === "BS") {
                    formulaInput.value = formulaInput.value.slice(0, -1);
                } else if (val === "-H") {
                    let formula = formulaInput.value;
                    let match = formula.match(/H(\d+)$/);
                    if (match) {
                        let num = parseInt(match[1]);
                        if (num > 2) { formula = formula.replace(/H\d+$/, "H" + (num - 1)); }
                        else if (num === 2) { formula = formula.replace(/H2$/, "H"); }
                        else if (num === 1) { formula = formula.replace(/H1$/, ""); }
                    } else if (formula.endsWith("H")) {
                        formula = formula.slice(0, -1);
                    }
                    formulaInput.value = formula;
                } else {
                    formulaInput.value += val;
                }
                calculate(formulaInput.value);
            });
        });

        // --- 周期表データ ---
        const periodicTableGrid = [
            ["H", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "He"],
            ["Li", "Be", "", "", "", "", "", "", "", "", "", "", "B", "C", "N", "O", "F", "Ne"],
            ["Na", "Mg", "", "", "", "", "", "", "", "", "", "", "Al", "Si", "P", "S", "Cl", "Ar"],
            ["K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr"],
            ["Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe"],
            ["Cs", "Ba", "La", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn"],
            ["Fr", "Ra", "Ac", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"],
            ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
            ["", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "", "", ""],
            ["", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "", "", ""]
        ];
        const colors = {
            "H": "nonmetal", "He": "noble",
            "Li": "alkali", "Na": "alkali", "K": "alkali", "Rb": "alkali", "Cs": "alkali", "Fr": "alkali",
            "Be": "alkaline", "Mg": "alkaline", "Ca": "alkaline", "Sr": "alkaline", "Ba": "alkaline", "Ra": "alkaline",
            "Sc": "transition", "Ti": "transition", "V": "transition", "Cr": "transition", "Mn": "transition", "Fe": "transition", "Co": "transition", "Ni": "transition", "Cu": "transition", "Zn": "transition",
            "Y": "transition", "Zr": "transition", "Nb": "transition", "Mo": "transition", "Tc": "transition", "Ru": "transition", "Rh": "transition", "Pd": "transition", "Ag": "transition", "Cd": "transition",
            "La": "lanthanoid", "Ce": "lanthanoid", "Pr": "lanthanoid", "Nd": "lanthanoid", "Pm": "lanthanoid", "Sm": "lanthanoid", "Eu": "lanthanoid", "Gd": "lanthanoid", "Tb": "lanthanoid", "Dy": "lanthanoid", "Ho": "lanthanoid", "Er": "lanthanoid", "Tm": "lanthanoid", "Yb": "lanthanoid", "Lu": "lanthanoid",
            "Ac": "actinoid", "Th": "actinoid", "Pa": "actinoid", "U": "actinoid", "Np": "actinoid", "Pu": "actinoid", "Am": "actinoid", "Cm": "actinoid", "Bk": "actinoid", "Cf": "actinoid", "Es": "actinoid", "Fm": "actinoid", "Md": "actinoid", "No": "actinoid", "Lr": "actinoid",
            "B": "metalloid", "Si": "metalloid", "Ge": "metalloid", "As": "metalloid", "Sb": "metalloid", "Te": "metalloid",
            "C": "nonmetal", "N": "nonmetal", "O": "nonmetal", "P": "nonmetal", "S": "nonmetal", "Se": "nonmetal",
            "F": "halogen", "Cl": "halogen", "Br": "halogen", "I": "halogen", "At": "halogen", "Ts": "halogen",
            "Ne": "noble", "Ar": "noble", "Kr": "noble", "Xe": "noble", "Rn": "noble", "Og": "noble"
        };
        const periodicTableDiv = document.getElementById("periodicTable");
        periodicTableGrid.forEach(row => {
            row.forEach(el => {
                const btn = document.createElement("button");
                btn.className = "btn";
                if (el) {
                    btn.textContent = el;
                    if (colors[el]) btn.classList.add(colors[el]);
                    btn.addEventListener("click", () => {
                        formulaInput.value += el;
                        calculate(formulaInput.value);
                    });
                } else {
                    btn.style.visibility = "hidden";
                }
                periodicTableDiv.appendChild(btn);
            });
        });
    </script>

    <hr>
    <div style="text-align:center; margin-top:20px;">
        <button id="convertBtn"
                style="background-color:#4CAF50;color:white;padding:8px 12px;
           border:none;border-radius:4px;cursor:pointer;">
            🔄 原子量JSON再生成(管理者用)
        </button>
    </div>

    <script>
        document.getElementById("convertBtn").addEventListener("click", async () => {
            if (!confirm("atomic_weights.json を再生成しますか?")) return;
            try {
                const res = await fetch("/convert_csv");
                const data = await res.json();
                alert(`${data.status}: ${data.message}\n変換件数: ${data.count || "不明"}`);
            } catch (e) {
                alert("変換エラー: " + e);
            }
        });
    </script>

</body>
</html>

従来通り、分子式入力欄・機能基登録・計算結果表示部分に変更はありません。
ただし、JSON変換後の再読み込みを行うことで、
最新の原子量データを即座に反映できます。


👨‍🔬 所長のコメント

「なるほど、原子量ひとつ取っても奥が深い!
範囲を中央値で扱うのは実用的でありながら、科学的にも誠実な選択ですね。
カスタム官能基も保存できるとは、分二郎もいよいよ研究室仕様になってきました。」


✅ 今回のまとめ

  • ver.7.7 では、カスタム官能基・芳香環の管理機能を実装

  • 同位体比変動を考慮し、範囲をもつ元素には中央値を採用

  • atomic_weights.csv → atomic_weights.json の再生成機能を追加

  • 教育・実用の両立を目指した設計


🔜 次回予告

👉 第12回は「分二郎のパッケージ化・配布」を扱います。
Flaskアプリをスタンドアロン化し、研究室・授業環境で簡単に導入できるよう整理します。
requirements.txt や exe 化、Mac対応なども詳説します。



いいなと思ったら応援しよう!