03How scientific workflows become software

Scientific objects cross typed software contracts.

~18 min reading10 sections3 knowledge checks

Progress is saved only in this browser. No account or tracking.

The object is the handoff

Concept plate 03
MoleculeAlgorithmStateTyped inputSettings + methodCapabilitiesMeaning travels across the API boundary
A molecular model enters an electronic solver, which returns an energy and state for the next consumer. Algorithm selection does not erase each object’s scientific meaning.
  • 1Input: molecule and representation
  • 2Tool: registered family + implementation
  • 3Output: energy and wavefunction
Original technical lesson · 1 contextual figure addedDownload original MarkdownJump to the interactive model

The calculation is a chain of scientific handoffs

We have chosen neutral, singlet nitrogen at a bond length of 1.85 Å. We also know that a molecular geometry, a set of orbitals, a many-electron state, and a Hamiltonian are different things. The software architecture makes those distinctions useful: each stage receives a particular scientific object and produces another one. A later stage should not need to know which internal loops produced the earlier object.

Imagine handing another colleague a wavefunction. A spreadsheet of unexplained numbers is insufficient. They need the orbital basis, electron occupations, coefficient conventions, and a way to determine which information is actually present. QDK Chemistry packages these together in data objects. An algorithm object is the separately configured processor that creates or transforms them.

A class describes a kind of object: Structure describes molecular structural data. An object, or instance, is one particular structure, such as our stretched nitrogen molecule. A function is a named operation with inputs and a result. A method is a function attached to an object or class: wavefunction.get_orbitals() retrieves the orbitals associated with that wavefunction. An interface is the agreement about which operations exist and what they accept and return. A backend is the implementation doing the work behind that agreement. These programming words matter because they tell us what can be exchanged without rewriting the science workflow.

The architecture guide expresses the intended separation as immutable data and configurable, stateless algorithms. Treat this as a design principle, not a claim that every operation is free of caches, randomness, external services, or implementation-specific behavior.

Four decisions that must not be confused

Consider the first electronic calculation. Choosing Hartree–Fock selects a scientific approximation: represent the electrons by a single determinant and optimize its orbitals. Choosing cc-pVDZ selects the finite one-electron representation in which those orbitals are expanded. Choosing an SCF optimization strategy such as DIIS or the native DIIS/GDM combination selects a numerical route toward a stationary solution. Choosing implementation qdk rather than pyscf selects software that carries out the task.

Those choices interact, but they are not synonyms. A different backend can implement the same Hartree–Fock model; a different basis changes the model space even when the backend is unchanged. Two solvers with the same interface may use different convergence tests, integration grids, initial guesses, or orbital orderings. Interface compatibility means their objects fit the same handoff. It does not certify identical energies, spin behavior, numerical precision, or scientific conclusions.

Change one choice and know what changed#

Supplementary · Conceptual diagram

Physical approximation
Hartree–Fock

What class of many-electron state?

Representation
cc-pVDZ

What finite orbital vocabulary?

Numerical strategy
DIIS / GDM

How do we seek stationarity?

Implementation
qdk / pyscf

Which supported software carries it out?

Model and source. Examples are those named in this section. Equal API contracts do not imply equal implementations, stopping criteria, or results.

How to read this. A basis choice changes the representational space; a backend choice changes who implements a task. Those are not the same experiment.

Return to this explanation

Here is the data journey we will gradually make operational:

Handoff Scientific object being passed
Molecular input → SCF Structure, charge, spin multiplicity, and a basis or orbital guess
SCF → classical analysis A total energy and Wavefunction, with associated Orbitals
Orbital/model selection → integral construction Orbitals, including the basis and relevant orbital-space information
Integral construction → many-electron or qubit methods Hamiltonian, rather than just the SCF energy
Fermion-to-qubit translation QubitOperator, carrying an encoding convention
Trial-state construction Circuit for preparing a selected Wavefunction
Hamiltonian-based circuit construction A UnitaryRepresentation, then circuit representations
Phase estimation and execution A QpeResult; the circuit and workload also feed resource analysis

This is not one compulsory straight-line script. Classical solvers and quantum solvers are alternative consumers of a model Hamiltonian. State preparation and Hamiltonian compilation are two branches that must agree when joined. Resource analysis can inspect circuits without pretending they were executed on a quantum device. Active-space selection, correlated wavefunctions, encodings, and execution deserve their own later lessons.

The concrete Hamiltonian-constructor contract accepts Orbitals and returns Hamiltonian. The unitary-builder interface accepts QubitOperator and returns UnitaryRepresentation. The phase-estimation interface accepts a preparation Circuit and QubitOperator, with optional noise information, and returns QpeResult. Notice that a common verb, run, does not imply common arguments.

Asking the registry for the right tool

A registry is a catalog mapping names to implementations. A factory consults that catalog and constructs a new algorithm object. The Python entry point combines the two ideas:

from qdk_chemistry.algorithms import (
    available, create, inspect_settings, show_default,
)

scf_names = available("scf_solver")
default_scf = show_default("scf_solver")
schema = inspect_settings("scf_solver", "qdk")
solver = create("scf_solver", "qdk", max_iterations=80)

These examples explain API use in an ordinary installed environment; no imports or calculations were executed while preparing this lesson. available() with no argument returns a dictionary of algorithm types and their available implementations. With "scf_solver", it returns a list. show_default reports the configured default, not a benchmark-based recommendation. At this revision, ScfSolverFactory.default_algorithm_name() is "qdk". The native factory also registers "qdk_stabilized", a composite stability-checking workflow that we will study in Lesson 6. PySCF names appear only when that integration is successfully registered.

create("scf_solver"), create("scf_solver", None), and an empty implementation name select the factory default. The default is explicitly declared: it is not whichever implementation happens to appear first in a list. inspect_settings returns records containing a setting name, expected Python type, default value, description, and constraints where present. print_settings provides a human-readable table. Discovery is safer than copying an old example table whose keys may no longer match the implementation.

The source of truth is the Python registry, backed for native algorithms by the C++ factory implementation. The registry tests explicitly check default creation, named creation, keyword settings, and invalid-name errors. These are source-level checks of the intended behavior, not tests newly run for this course.

The first real contract: molecule in, energy and state out

Using the structure, charge, and spin_multiplicity defined for our nitrogen example, the first handoff is:

solver = create("scf_solver", "qdk", method="hf")
solver.settings().set("convergence_threshold", 1e-7)

energy, wavefunction = solver.run(
    structure,
    charge=charge,
    spin_multiplicity=spin_multiplicity,
    basis_or_guess="cc-pvdz",
)
orbitals = wavefunction.get_orbitals()
hamiltonian = create("hamiltonian_constructor", "qdk").run(orbitals)

The two names on the left of the SCF call unpack a tuple, an ordered bundle of results. The first item is a floating-point total energy in Hartree; the second is a Wavefunction. orbitals is obtained from that object, not mistaken for the second return value itself. The later Hamiltonian call returns one object, so it is not unpacked into the same pair.

The SCF interface makes all four inputs explicit. Charge and multiplicity are run arguments, not universal settings. basis_or_guess accepts a basis-name string, an explicit BasisSet, or Orbitals containing a reusable orbital guess and its basis. An algorithm setting called basis_set should not be invented because an old demonstration table happens to contain it.

The distinction between arguments and settings is practical. Arguments identify the problem data for this run; settings configure how this algorithm tackles it. The same configured solver may be run again on compatible new inputs. Changing its numerical configuration after execution is a different matter.

Settings are typed controls, not an unrestricted dictionary

settings() returns the algorithm-owned settings object. get("max_iterations") reads a value; set changes one existing key; update can apply several overrides; to_dict() gives a readable snapshot. Factory keyword arguments are forwarded to the same update mechanism. They are not miscellaneous constructor parameters forwarded to arbitrary third-party software.

The native base ElectronicStructureSettings defines method="hf", convergence_threshold=1e-7, max_iterations=50, and scf_type="auto". The native implementation extends that schema with settings such as eri_method, enable_gdm, and nthreads. These are real implementation-dependent controls. PySCF has its own extended schema, so an option accepted by one backend need not be accepted by another.

The C++ store distinguishes booleans, signed 64-bit integers, floating-point values, strings, several vector types, and nested AlgorithmRef values. A setting can have a numeric bound or an allowed-value list. For example, max_iterations must be at least one and native eri_method is one of "direct" and "incore". Use 80 for an integer count and 1e-7 for a floating-point threshold. The Python binding deliberately rejects a boolean where an integer is expected, despite Python normally treating booleans as integer-like.

Importantly, the actual Settings::set implementation checks existing keys, type compatibility, and declared limits when setting values. The documentation phrase “validated at execution time” is not the whole story. Other checks require the molecule or a combination of settings and therefore happen only during execution. A valid string type cannot prove that a named basis file exists or that a requested method supports a particular spin case.

Both the C++ algorithm base and Python algorithm base implement ordinary execution as “lock settings, then call _run_impl.” The leading underscore marks implementation machinery, not the method a course workflow should call directly.

Locking happens before computation, not only after success. Even a failing underlying run has already locked its settings. A later attempt to change them raises SettingsAreLocked; make a new solver to alter the configuration. Reusing a locked solver with the same settings and new appropriate arguments is not forbidden. The lock is a configuration safeguard, not a declaration that every algorithm object is thread-safe.

There are two precise architectural qualifications. The registry wraps algorithms to support optional cache and remote execution; a cache hit can return without entering the underlying run, so the lock behavior described above applies to actual underlying execution. Also, Settings::lock() sets that settings object’s lock flag; it is not a recursively enforced freeze of every separately reachable nested object. Configure the entire computation before running and do not mutate retained nested settings afterward. The intended usage discipline is stronger than a claim of universal deep immutability.

Following the native call across the language boundary

Now trace the nitrogen call without getting lost in integral mathematics.

The Python name ScfSolver is re-exported from the compiled _core._algorithms extension. The pybind11 SCF binding exposes run(structure, charge, spin_multiplicity, basis_or_guess) and converts the C++ pair of energy and wavefunction pointer into the Python tuple. pybind11 is the bridging library that makes C++ classes callable from Python; it is not another chemical approximation.

The Python registry finds the C++ ScfSolverFactory. Its registration source supplies a creator for microsoft::ScfSolver. That concrete implementation reports its name as "qdk". After the base locks its settings, the native adapter resolves the basis, converts structural data to the internal engine, selects the HF or KS solver, constructs an initial density, and runs the internal SCF calculation.

When that engine returns, the adapter checks convergence rather than silently labeling any final iterate a solution. It maps the internal shell ordering back to the QDK basis ordering. It creates Orbitals containing coefficients, orbital energies, overlap information, and the basis. It then constructs a canonical occupied Configuration, a StateVectorContainer, and finally a Wavefunction. The energy is taken from the engine’s total SCF energy. The KS path also packages its orbital determinant this way; packaging a KS reference does not make it an exact interacting many-electron state.

A C++ shared_ptr is a shared ownership handle: several objects can refer to the same data while its lifetime remains valid. It is not “copy all matrices now.” The binding uses lifetime-aware holders and returns the settings with a policy that keeps their owner alive. These engineering details let Python code hold a wavefunction or its orbitals without manually managing C++ memory. They do not excuse modifying somebody else’s scientific input.

The binding also has a trampoline, a bridge that lets a Python subclass implement a C++ virtual method such as _run_impl. That is how a Python-written SCF adapter can satisfy the native interface. The PySCF implementation inherits this bound ScfSolver, instead of requiring the entire chemistry pipeline to adopt PySCF-specific objects.

Immutable scientific records, with carefully distinguished caches

The word immutable means that a data object’s defining scientific information is not normally rewritten after construction. To rotate orbitals or select a different space, an algorithm returns a new object. Keeping the old object allows comparisons and preserves the meaning of calculations that already used it.

The Python DataClass base marks an object initialized at the end of construction and rejects ordinary attribute assignment and deletion afterward. C++ data classes expose their own non-mutating interfaces. Do not generalize this into a promise that every nested Python dictionary, foreign-library object, or array in every subclass is recursively frozen.

Lazy derived information is different from defining information. A lazy cache computes a derived value when first requested and retains it to avoid repeating work. The native wavefunction/container API has caches for derived reduced-density information. A particularly visible example is Circuit.get_qir(): when a Q# factory is available, it can compile and internally cache QIR using an explicit internal attribute assignment, although user reassignment such as circuit.qasm = ... is rejected. The circuit tests check ordinary immutability.

“Self-contained” likewise needs interpretation. Some orbital properties are optional; not every wavefunction container can provide determinant coefficients or all reduced density matrices. A consumer must require the capabilities it actually uses. Common inheritance alone does not manufacture missing scientific data.

There is also a naming trap. A chemical Configuration, such as the occupation string 2du0, records particle occupations in an orbital/mode convention. Algorithm configuration means settings such as a convergence threshold. They are unrelated uses of the same English word.

Composing tools rather than hard-coding them

An AlgorithmRef records a nested algorithm type, implementation name, and optional settings. It describes what to construct later; it is not itself a running solver. For example:

from qdk_chemistry.data import AlgorithmRef

stabilized = create("scf_solver", "qdk_stabilized", max_iterations=80)
stabilized.settings().set(
    "scf_solver",
    AlgorithmRef("scf_solver", "qdk", eri_method="incore"),
)

Here a composite SCF workflow will use a native SCF child with in-memory integrals. This illustrates software composition, not a recommendation that incore is best for nitrogen. AlgorithmRef can resolve default child settings through the registry and validate overrides. Its algorithm type is fixed; changing its implementation name re-resolves defaults and discards prior overrides. The nested-settings tests cover construction, type protection, and multi-level references.

Read the composition policy too. In qdk_stabilized, common parent SCF settings are copied onto the newly created child after nested creation. Thus the parent’s max_iterations takes precedence over a competing child override of that common key. The child-only eri_method in this example is a cleaner illustration. This wrapper also creates a stability checker and can rerun SCF using modified orbitals; the physical meaning comes in Lesson 6.

Custom integrations register a creator callable, not one already-used shared solver: register(lambda: MySolver()) means “construct a fresh instance when requested.” For native SCF integration, MySolver should implement the bound SCF interface, including the full four-argument contract and the energy/wavefunction pair. A new algorithm type needs its own factory. Existing names and aliases cannot be silently hijacked: registration raises DuplicateRegistrationError on conflicts.

One genuinely Python-built quantum path

Classical C++ is not a mandatory intermediate language for every algorithm. Consider create("state_prep", "dense_pure_state"). This is a registered Python implementation of StatePreparation; its input is a coefficient-bearing Wavefunction and its output is Circuit.

The common helper reads coefficients and active determinants. It does not assume the coefficient list already sits at the correct bit-string indices. It derives each determinant’s index from occupation bits and scatters the coefficient into a dense array. Non-negligible imaginary coefficients are rejected by this real-amplitude path, and widths greater than 32 qubits are refused.

The concrete implementation constructs Q# parameters, including a reversed register-row map, and obtains a callable plus factory data. The repository’s Q# operation delegates dense amplitude loading to Std.StatePreparation.PreparePureStateD. Chemistry code therefore supplies scientific amplitudes and ordering; the external QDK/Q# machinery supplies language execution and compilation. The returned circuit declares encoding="jordan-wigner" and a register width. It need not already contain an OpenQASM string, despite a broad base-method docstring describing one.

This is a method with a real resource tradeoff, even before quantum execution: a dense array has 2q2^q entries for qq qubits. An illustrative four-spatial-orbital electron problem uses eight occupation qubits and 256 entries. At 32 qubits, just one float64 array occupies 32 GiB; the guard is not a promise that every accepted input is affordable. Our full 28-spatial-orbital nitrogen representation would need 56 occupation qubits and is outside this dense implementation’s limit. We are tracing an interface on a small example, not proposing this full-molecule call. Later state-preparation lessons will compare sparse and other approaches.

The dense-preparation tests compare prepared states against determinant-derived expectations, including bit ordering. That is why metadata matters: a circuit preparing Jordan–Wigner occupation bits is not automatically compatible with a Hamiltonian using parity or Bravyi–Kitaev encoding merely because both objects have the same qubit count.

Plugins, identity, and useful failures

The plugin API exposes QdkChemistryPlugin with API version 1 and a PluginRegistrar. An external installed package can advertise an entry point in the qdk_chemistry.plugins group. Registration can contribute algorithms, new factories, data-class loaders, remote backends, and cache backends. The loader validates the plugin class/version and warns when an external plugin fails to load. Bundled optional integrations have a separate autoload path; missing optional imports are handled differently from duplicate registrations.

The data registry maps a stable serialized type name to a loader, rather than selecting a computational method. A plugin returning a new data class must supply that loading contract if another process is to reconstruct its results. Full serialization and remote execution come later.

Source presence is not installed availability. The package metadata makes PySCF an optional integration and excludes that dependency from the plugins extra on Windows. An environment’s loaded registry is the relevant runtime catalog, not a promise that every file in this repository is usable on this machine. For this read-only preparation, we inspected source and metadata instead of importing the package, installing plugins, or triggering resource/stub initialization.

Algorithms also expose hash(...), using the run’s algorithm identity, settings, and inputs to identify computations for caching. Native and Python hashing have implementation-specific encodings; do not assume interchangeable hashes across languages, versions, or different argument-passing forms. A short content hash is a bookkeeping identifier, not proof of chemical equivalence, absence of collisions, or bitwise reproducibility across machines. Preserve explicit geometry, units, charge, basis, method, implementation, settings, and dependency provenance too.

Failures should be read at the correct layer. KeyError from registry creation points to a missing type or implementation. SettingNotFound and SettingTypeMismatch identify configuration mistakes. An out-of-range value can raise ValueError. A native RuntimeError saying SCF did not converge means the computation did not satisfy its numerical stopping conditions; it is not a request to substitute a plausible energy. A missing circuit conversion dependency is a capability failure, not a bad molecule.

We can now explain why exchanging SCF engines need not rewrite downstream chemistry: each adapter returns the same scientific kind of object, with explicit conventions to check. We can also see what that architecture cannot decide for us. The next step is to make the molecule and its one-electron representation unambiguous before any solver begins work.

Make the idea your own

Supplementary learning

The original lesson above is preserved in full, with labeled supplemental figures placed beside the relevant explanations. The model and questions below are also learning aids, not newly executed chemistry results.

Inspect a scientific handoff

Interactive model

Inspect the minimum information another team needs to interpret a result.

Illustrative model, not a scientific calculation. A teaching checklist for information sufficiency, not validation of a chemistry result or a remote execution. Checking boxes does not certify that the information itself is correct.

What does this hypothetical record include?

Enable JavaScript to explore this model. The assumptions and original lesson remain readable without it.

Check your understanding

Supplementary learning

Reason through these 3 questions. Your answers do not mark the lesson as read, and nothing is locked.

1. What does selecting an implementation from a registry resolve?
2. Why should settings be typed instead of an unrestricted dictionary?
3. What must a plugin preserve when implementing the same algorithm family?

Enable JavaScript to check and save answers locally.

Source notes & references (41)

Source-traced at 97a3ff4f; no imports, execution, installation, tests, or scientific workflows run. Architecture ideals are distinguished from shallow attribute protection, internal caches, root-only settings locks, and cache-hit bypass of underlying run. Availability is environment-dependent; no claim PySCF is installed on Windows. Dense Q# path is a small-system interface illustration, not a viable full 56-qubit N2 preparation. Later methods and resource estimation internals are intentionally deferred.

Public source: microsoft/qdk-chemistry, pinned to 97a3ff4fb86e. This course describes that revision, not every later release.

Look up a term

On this page

  1. The calculation is a chain of scientific handoffs
  2. Four decisions that must not be confused
  3. Asking the registry for the right tool
  4. The first real contract: molecule in, energy and state out
  5. Settings are typed controls, not an unrestricted dictionary
  6. Following the native call across the language boundary
  7. Immutable scientific records, with carefully distinguished caches
  8. Composing tools rather than hard-coding them
  9. One genuinely Python-built quantum path
  10. Plugins, identity, and useful failures
  11. Interactive model
  12. Knowledge check
  13. Source notes

The complete journey

Course homeGlossarySavedAbout

Search the course

Search titles and the full lesson text.

    Tab to a result, Enter to open. Escape closes search.