Tutorials

MarketSolve is called from ASE like any other calculator. The calculation happens on the marketplace and the result comes back verified. Seven examples, easy to hard, matching the reference problems.

Contents
  1. Bulk MgO: the synchronous calculator
  2. Bulk Fe: collinear magnetism
  3. Defected SrTiO₃: constraints, relaxation, resumable jobs
  4. NiO: noncollinear spin, SOC, Hubbard U, explicit parameters
  5. Co(110) slab: per-axis constraints and vacuum
  6. Fe/MgO/Fe junction: the magnetic state is the claim
  7. Au/MoS₂/Au memristor: a matched pair

1. Bulk MgO: the synchronous calculator

Submit, wait, get a verified number back.

from ase.build import bulk
from marketsolve_client import MarketSolve

atoms = bulk("MgO", crystalstructure="rocksalt", a=4.21)
atoms.calc = MarketSolve(api_key="msk_...", engine="siesta", fidelity="reasonable")

e = atoms.get_potential_energy()   # submits, waits for verification, returns eV
f = atoms.get_forces()             # cached: same frozen problem, no second charge

print(atoms.calc.oracle_result.acceptance_json)
If verification fails the calculator raises. You cannot receive an unverified energy. The complete raw solver log and its hash are on oracle_result.

2. Bulk Fe: collinear magnetism

Magnetism uses ASE's standard channel. Smearing and mixing come from the fidelity preset. You declare the physics, not the solver settings.

atoms = bulk("Fe", "bcc", a=2.87)
atoms.set_initial_magnetic_moments([2.2] * len(atoms))

atoms.calc = MarketSolve(api_key="msk_...", engine="qe", fidelity="conservative")
e = atoms.get_potential_energy()

3. Defected SrTiO₃: constraints, relaxation, resumable jobs

A bounty can take hours to clear, so use the job API instead of the blocking calculator. Constraints come from atoms.constraints.

from ase.constraints import FixAtoms
from marketsolve_client import Client

sto = make_srtio3_supercell(3, 3, 3)          # a = 3.905 Å
del sto[oxygen_vacancy_index]
sto.constraints = [FixAtoms(indices=outer_shell(sto))]

client = Client(api_key="msk_...")
job = client.submit(sto, engine="siesta", mode="relax",
                    fidelity="reasonable", bounty_usd=0.047)
print(job.id)                                  # persist it; safe to exit

# later, in any process:
result = client.job(job.id).result(wait=True)
print(result.energy_ev, result.final_geometry)
An unsupported constraint type is a hard error at submit, because a dropped constraint changes the answer. Supported: FixAtoms, FixedLine, FixedPlane, FixCartesian.

4. NiO: noncollinear spin, SOC, Hubbard U, explicit parameters

ASE has no vector-magmom channel, so the client adds one. Setting soc=True selects the fully relativistic pseudopotential set.

from marketsolve_client import Client, set_vector_magmoms, QeParams

nio = make_nio_afm2_cell()                     # rocksalt, AFM-II ordering
set_vector_magmoms(nio, afm2_vectors(nio))     # stored in atoms.info

job = Client(api_key="msk_...").submit(
    nio, mode="scf", soc=True,
    params=QeParams(ecutwfc_ry=..., kgrid=...,
                    hubbard=[{"species": "Ni", "orbital": "3d", "u_ev": ...}]),
    allow_more_accurate=True)
This is the explicit-parameters path. The tuple you pass is frozen verbatim into the problem record. Pseudopotentials are chosen by the sponsor: a named set (pseudodojo-sr by default, pseudodojo-fr when soc=True), or your own files uploaded via POST /v1/pseudos and selected by id. Per-element hashes are frozen at posting. A solver cannot substitute pseudos.

5. Co(110) slab: per-axis constraints and vacuum

A surface needs a vacuum gap, which sends the k-grid to Γ along that axis. This one also relaxes in-plane only, and uses noncollinear spin with spin-orbit coupling.

from ase.build import fcc110
from ase.constraints import FixCartesian
from marketsolve_client import Client, set_vector_magmoms

slab = fcc110("Co", size=(3, 3, 7), a=3.544, vacuum=6.0)   # 12 Å between images
set_vector_magmoms(slab, [(0.0, 0.0, 1.7)] * len(slab))
# mask marks the FIXED directions: freeze z, relax x and y
slab.constraints = [FixCartesian(range(len(slab)), mask=(False, False, True))]

job = Client(api_key="msk_...").submit(
    slab, engine="qe", mode="relax", fidelity="reasonable",
    soc=True, bounty_usd=9.531)
record = job.result(wait=True)      # a bounty returns the public settlement record
Per-axis masks travel in the sidecar as fix_cartesian entries and are re-checked on the delivered geometry. A slab whose z coordinates moved fails regardless of its forces. soc=True requires noncollinear spin and rejects scalar-relativistic pseudos at posting. Species that are usually magnetic (Fe, Co, Ni, Mn, Cr, V and others) must declare a spin state; the platform never picks an ordering for you.

6. Fe/MgO/Fe junction: the magnetic state is the claim

A magnetic tunnel junction has two self-consistent states at the same geometry, electrodes parallel or antiparallel. Which one a calculation lands in is decided by the starting density. Forces cannot tell them apart.

from marketsolve_client import Client

mtj = build_fe_mgo_fe(n_fe=6, n_mgo=5)          # 88 atoms, 5.73 Å in-plane
moments = [+2.2 if is_bottom_fe(a) else -2.2 if is_top_fe(a) else 0.0 for a in mtj]
mtj.set_initial_magnetic_moments(moments)      # antiparallel

bounty = Client(api_key="msk_...").submit(
    mtj, engine="siesta", mode="scf", fidelity="conservative", bounty_usd=1.605)
This is why verification takes the submitted density rather than re-deriving one. The density says which state is being claimed. The oracle checks that it is converged under the frozen protocol and that its magnetic character matches the problem. A parallel state submitted to an antiparallel problem fails on character with perfect residuals. Sampled cold audits re-solve from the standard start to keep this honest.

7. Au/MoS₂/Au memristor: a matched pair

A monolayer MoS₂ memristor switches when an Au atom from the electrode drops into a sulfur vacancy. The number you want is the energy difference between the two states. That is two solves that must be comparable, which is the case for turning allowMoreAccurate off.

from marketsolve_client import Client

client = Client(api_key="msk_...")
hrs = build_au_mos2_au(vacancy=True, filled=False)   # 48 atoms: bare S vacancy
lrs = build_au_mos2_au(vacancy=True, filled=True)    # 49 atoms: Au in the vacancy

common = dict(engine="siesta", mode="relax", fidelity="conservative",
              allow_more_accurate=False)             # both members on one frozen spec

bounties = [client.submit(s, bounty_usd=0.216, **common) for s in (hrs, lrs)]
e_hrs, e_lrs = (b.result(wait=True)["result"]["energy_ev"] for b in bounties)
print(f"filament formation energy: {e_lrs - e_hrs:+.3f} eV")
With allowMoreAccurate on, a solver may deliver either member converged tighter than the frozen floor. That is fine for one number and wrong for a difference, because the subtraction then includes the gap between two different convergence levels. Off, both members are pinned to the same spec. The two cells differ by one atom, so this is a formation energy and needs a chemical potential reference.
The leads are periodic through the cell boundary rather than terminated with vacuum, so the Au behaves like bulk. Boundary layers are held with FixAtoms; everything near the interface relaxes.

Two rules