diff --git a/changelog-entries/859.md b/changelog-entries/859.md new file mode 100644 index 000000000..78f262cf7 --- /dev/null +++ b/changelog-entries/859.md @@ -0,0 +1 @@ +- Added several simplification and fixes to the two-scale heat conduction case. Most prominently removing the data `K01` and `K10` [#859](https://github.com/precice/tutorials/pull/859) diff --git a/two-scale-heat-conduction/images/tutorials-two-scale-heat-conduction-precice-config.png b/two-scale-heat-conduction/images/tutorials-two-scale-heat-conduction-precice-config.png index f2cc2ad5e..b01fc1dc8 100644 Binary files a/two-scale-heat-conduction/images/tutorials-two-scale-heat-conduction-precice-config.png and b/two-scale-heat-conduction/images/tutorials-two-scale-heat-conduction-precice-config.png differ diff --git a/two-scale-heat-conduction/macro-dumux/appl/main.cc b/two-scale-heat-conduction/macro-dumux/appl/main.cc index 8c1870b64..45a6e9b35 100644 --- a/two-scale-heat-conduction/macro-dumux/appl/main.cc +++ b/two-scale-heat-conduction/macro-dumux/appl/main.cc @@ -149,18 +149,14 @@ int main(int argc, char **argv) // initialize the coupling data std::string readDatak00; - std::string readDatak01; - std::string readDatak10; std::string readDatak11; std::string readDataPorosity; std::string writeDataConcentration; if (runWithCoupling) { readDatak00 = couplingParticipant.getReadDataNamesOnMesh(meshName)[0]; - readDatak01 = couplingParticipant.getReadDataNamesOnMesh(meshName)[1]; - readDatak10 = couplingParticipant.getReadDataNamesOnMesh(meshName)[2]; - readDatak11 = couplingParticipant.getReadDataNamesOnMesh(meshName)[3]; - readDataPorosity = couplingParticipant.getReadDataNamesOnMesh(meshName)[4]; + readDatak11 = couplingParticipant.getReadDataNamesOnMesh(meshName)[1]; + readDataPorosity = couplingParticipant.getReadDataNamesOnMesh(meshName)[2]; writeDataConcentration = couplingParticipant.getWriteDataNamesOnMesh(meshName)[0]; } @@ -191,8 +187,6 @@ int main(int argc, char **argv) std::vector kInitial(numberOfElements, 1.0); std::vector porosityInitial(numberOfElements, 0.5); couplingParticipant.writeQuantityVector(meshName, readDatak00, kInitial); - couplingParticipant.writeQuantityVector(meshName, readDatak01, kInitial); - couplingParticipant.writeQuantityVector(meshName, readDatak10, kInitial); couplingParticipant.writeQuantityVector(meshName, readDatak11, kInitial); couplingParticipant.writeQuantityVector(meshName, readDataPorosity, porosityInitial); @@ -211,8 +205,6 @@ int main(int argc, char **argv) // add model specific output fields vtkWriter.addField(problem->getPorosity(), "Porosity"); vtkWriter.addField(problem->getK00(), "K00"); - vtkWriter.addField(problem->getK01(), "K01"); - vtkWriter.addField(problem->getK10(), "K10"); vtkWriter.addField(problem->getK11(), "K11"); problem->updateVtkOutput(x); vtkWriter.write(0.0); @@ -286,10 +278,6 @@ int main(int argc, char **argv) // and coupling data is interpolated in time couplingParticipant.readQuantityFromOtherSolver(meshName, readDatak00, dt); - couplingParticipant.readQuantityFromOtherSolver(meshName, readDatak01, - dt); - couplingParticipant.readQuantityFromOtherSolver(meshName, readDatak10, - dt); couplingParticipant.readQuantityFromOtherSolver(meshName, readDatak11, dt); couplingParticipant.readQuantityFromOtherSolver(meshName, diff --git a/two-scale-heat-conduction/macro-dumux/appl/spatialparams.hh b/two-scale-heat-conduction/macro-dumux/appl/spatialparams.hh index 6e6f641bb..3fc44e7af 100644 --- a/two-scale-heat-conduction/macro-dumux/appl/spatialparams.hh +++ b/two-scale-heat-conduction/macro-dumux/appl/spatialparams.hh @@ -89,15 +89,16 @@ public: { DimWorldMatrix K; + // Off-diagonal terms of K are always zero because the grain microstructure is circular. + K[0][1] = 0.0; + K[1][0] = 0.0; + if (getParam("Precice.RunWithCoupling") == true) { K[0][0] = couplingData_[scv.elementIndex()][1]; - K[0][1] = couplingData_[scv.elementIndex()][2]; - K[1][0] = couplingData_[scv.elementIndex()][3]; - K[1][1] = couplingData_[scv.elementIndex()][4]; + K[1][1] = couplingData_[scv.elementIndex()][2]; + } else { K[0][0] = getParam("Component.SolidThermalConductivity"); - K[0][1] = 0.0; - K[1][0] = 0.0; K[1][1] = getParam("Component.SolidThermalConductivity"); } return K; @@ -115,10 +116,6 @@ public: couplingData_[elementIdx][1] = couplingParticipant_.getScalarQuantityOnFace("Macro-Mesh", "K00", elementIdx); couplingData_[elementIdx][2] = - couplingParticipant_.getScalarQuantityOnFace("Macro-Mesh", "K01", elementIdx); - couplingData_[elementIdx][3] = - couplingParticipant_.getScalarQuantityOnFace("Macro-Mesh", "K10", elementIdx); - couplingData_[elementIdx][4] = couplingParticipant_.getScalarQuantityOnFace("Macro-Mesh", "K11", elementIdx); } } @@ -131,10 +128,10 @@ public: private: Dumux::Precice::CouplingAdapter &couplingParticipant_; - Dune::BlockVector> couplingData_; + Dune::BlockVector> couplingData_; Dumux::VectorCommDataHandleEqual< typename GridGeometry::ElementMapper, - Dune::BlockVector>, + Dune::BlockVector>, /* Entity codimension = */ 0> couplingDataHandle_; }; diff --git a/two-scale-heat-conduction/macro-dumux/params.input b/two-scale-heat-conduction/macro-dumux/params.input index 2f1f642ea..26c34e138 100644 --- a/two-scale-heat-conduction/macro-dumux/params.input +++ b/two-scale-heat-conduction/macro-dumux/params.input @@ -49,8 +49,6 @@ participant_name = Macro precice_config_file_path = ../precice-config.xml interfaces.1.mesh_name = Macro-Mesh interfaces.1.read_data.1.name = K00 -interfaces.1.read_data.2.name = K01 -interfaces.1.read_data.3.name = K10 -interfaces.1.read_data.4.name = K11 -interfaces.1.read_data.5.name = Porosity +interfaces.1.read_data.2.name = K11 +interfaces.1.read_data.3.name = Porosity interfaces.1.write_data.1.name = Concentration diff --git a/two-scale-heat-conduction/macro-nutils/macro.py b/two-scale-heat-conduction/macro-nutils/macro.py index 5f4479a7e..ca49013f1 100644 --- a/two-scale-heat-conduction/macro-nutils/macro.py +++ b/two-scale-heat-conduction/macro-nutils/macro.py @@ -46,6 +46,7 @@ def main(): # Define Gauss points on entire domain as coupling mesh (volume coupling from macro side) couplingsample = topo.sample('gauss', degree=2) # mesh vertices are Gauss points + n_gp = couplingsample.eval('x_i' @ ns).shape[0] # number of gauss points vertex_ids = participant.set_mesh_vertices(mesh_name, couplingsample.eval(ns.x)) else: sqrphi = topo.integral((ns.phi - phi) ** 2, degree=1) @@ -114,10 +115,15 @@ def main(): # Read conductivity and apply it to the existing solution k_00_c = couplingsample.asfunction(participant.read_data(mesh_name, "K00", vertex_ids, dt)) - k_01_c = couplingsample.asfunction(participant.read_data(mesh_name, "K01", vertex_ids, dt)) - k_10_c = couplingsample.asfunction(participant.read_data(mesh_name, "K10", vertex_ids, dt)) k_11_c = couplingsample.asfunction(participant.read_data(mesh_name, "K11", vertex_ids, dt)) + # Not read from preCICE as the values are zero. Instead, they are set to zero directly. + # k_01_c = couplingsample.asfunction(participant.read_data(mesh_name, "K01", vertex_ids, dt)) + # k_10_c = couplingsample.asfunction(participant.read_data(mesh_name, "K10", vertex_ids, dt)) + + k_01_c = couplingsample.asfunction(np.zeros(n_gp)) + k_10_c = couplingsample.asfunction(np.zeros(n_gp)) + conductivity = function.asarray([[k_00_c, k_01_c], [k_10_c, k_11_c]]) sqrk = couplingsample.integral(((ns.k - conductivity) * (ns.k - conductivity)).sum([0, 1])) solk = solver.optimize('solk', sqrk, droptol=1E-12) diff --git a/two-scale-heat-conduction/micro-dumux/appl/micro_sim.cpp b/two-scale-heat-conduction/micro-dumux/appl/micro_sim.cpp index e2168bfa0..7ab35c739 100644 --- a/two-scale-heat-conduction/micro-dumux/appl/micro_sim.cpp +++ b/two-scale-heat-conduction/micro-dumux/appl/micro_sim.cpp @@ -76,8 +76,6 @@ class MicroSimulation { private: const double pi_ = 3.14159265358979323846; double _k_00; - double _k_01; - double _k_10; double _k_11; double _porosity; int sim_id; @@ -244,16 +242,12 @@ py::dict MicroSimulation::solve(py::dict macro_write_data, double dt) // calculate the conductivity tensor _k_00 = _cpProblem->calculateConductivityTensorComponent(0, 0); - _k_10 = _cpProblem->calculateConductivityTensorComponent(1, 0); - _k_01 = _cpProblem->calculateConductivityTensorComponent(0, 1); _k_11 = _cpProblem->calculateConductivityTensorComponent(1, 1); py::dict micro_write_data; // add micro_scalar_data and micro_vector_data to micro_write_data micro_write_data["K00"] = _k_00; - micro_write_data["K10"] = _k_10; - micro_write_data["K01"] = _k_01; micro_write_data["K11"] = _k_11; micro_write_data["Porosity"] = _porosity; micro_write_data["Grain-Size"] = std::sqrt((1 - _porosity) / pi_); diff --git a/two-scale-heat-conduction/micro-dumux/micro-manager-config.json b/two-scale-heat-conduction/micro-dumux/micro-manager-config.json index 8fc54133b..fedc6c371 100644 --- a/two-scale-heat-conduction/micro-dumux/micro-manager-config.json +++ b/two-scale-heat-conduction/micro-dumux/micro-manager-config.json @@ -4,7 +4,7 @@ "participant_name": "Micro-Manager", "precice_config_file_name": "../precice-config.xml", "macro_mesh_name": "Macro-Mesh", - "write_data_names": ["K00", "K01", "K10", "K11", "Porosity"], + "write_data_names": ["K00", "K11", "Porosity"], "read_data_names": ["Concentration"] }, "simulation_params": { @@ -18,7 +18,8 @@ "history_param": 0.1, "coarsening_constant": 0.2, "refining_constant": 0.05, - "similarity_measure": "L2rel" + "similarity_measure": "L2rel", + "lazy_initialization": true } }, "diagnostics": { diff --git a/two-scale-heat-conduction/micro-nutils/micro-manager-config.json b/two-scale-heat-conduction/micro-nutils/micro-manager-config.json index 72125fb0a..7e882c6c9 100644 --- a/two-scale-heat-conduction/micro-nutils/micro-manager-config.json +++ b/two-scale-heat-conduction/micro-nutils/micro-manager-config.json @@ -4,7 +4,7 @@ "participant_name": "Micro-Manager", "precice_config_file_name": "../precice-config.xml", "macro_mesh_name": "Macro-Mesh", - "write_data_names": ["K00", "K01", "K10", "K11", "Porosity"], + "write_data_names": ["K00", "K11", "Porosity"], "read_data_names": ["Concentration"] }, "simulation_params": { @@ -18,7 +18,8 @@ "history_param": 0.1, "coarsening_constant": 0.2, "refining_constant": 0.05, - "similarity_measure": "L2rel" + "similarity_measure": "L2rel", + "lazy_initialization": true } }, "diagnostics": { diff --git a/two-scale-heat-conduction/micro-nutils/micro.py b/two-scale-heat-conduction/micro-nutils/micro.py index 6fff2aa38..7cd335bb9 100644 --- a/two-scale-heat-conduction/micro-nutils/micro.py +++ b/two-scale-heat-conduction/micro-nutils/micro.py @@ -5,8 +5,8 @@ """ import math -from nutils import mesh, function, solver, export, cli -import treelog +from nutils import mesh, function, solver, cli +# import treelog, export import numpy as np from copy import deepcopy @@ -16,6 +16,11 @@ class MicroSimulation: def __init__(self, sim_id): """ Constructor of MicroSimulation class. + + Parameters + ---------- + sim_id : int + Unique global ID assigned to this micro simulation instance by the Micro Manager. """ self._sim_id = sim_id @@ -44,6 +49,18 @@ def __init__(self, sim_id): self._k_nm1 = None # Average effective conductivity of last time step def initialize(self): + """ + Set up the initial phase field and solve the heat cell problem on a refined mesh. + + Computes the analytical initial grain geometry, refines the mesh around the + grain interface, and solves the cell problem to obtain initial effective + conductivity and porosity values. + + Returns + ------- + dict + Initial output data with keys ``K00``, ``K11``, and ``Porosity``. + """ # Define initial namespace self._ns = function.Namespace() self._ns.x = self._geom @@ -70,7 +87,6 @@ def initialize(self): # Initialize phase field once more on refined topology solphi = self._get_analytical_phasefield(self._topo, self._ns, self._degree_phi, self._ns.lam, self._r_initial) - self._solphi = solphi # Save solution of phi psi = self._get_avg_porosity(self._topo, solphi) self._psi_nm1 = psi # Average porosity value of last time step @@ -89,6 +105,17 @@ def initialize(self): return output_data def _reinitialize_namespace(self, topo): + """ + Rebuild the nutils function namespace for a given (possibly refined) topology. + + Called whenever the mesh changes (after each refinement step) to keep all + basis functions and symbolic expressions consistent with the current topology. + + Parameters + ---------- + topo : nutils.topology.Topology + The topology (coarse or hierarchically refined) to build the namespace for. + """ self._ns = None # Clear old namespace self._ns = function.Namespace() self._ns.x = self._geom @@ -115,10 +142,56 @@ def _reinitialize_namespace(self, topo): @staticmethod def _analytical_phasefield(x, y, r, lam): + """ + Evaluate the analytical diffuse-interface phase field at coordinates (x, y). + + Uses a smooth step function centred on the circle of radius ``r``, with + interface width controlled by ``lam``. + + Parameters + ---------- + x : array_like + X coordinates. + y : array_like + Y coordinates. + r : float + Radius of the grain. + lam : float + Diffuse interface width. + + Returns + ------- + array_like + Phase field value in [0, 1]: 1 inside the grain, 0 outside. + """ return 1. / (1. + np.exp(-4. / lam * (np.sqrt(x ** 2 + y ** 2) - r))) @staticmethod def _get_analytical_phasefield(topo, ns, degree_phi, lam, r): + """ + Project the analytical phase field onto the FE basis of the given topology. + + Minimises the L2 error between the analytical expression and the discrete + representation to obtain DOF coefficients. + + Parameters + ---------- + topo : nutils.topology.Topology + Topology on which the projection is performed. + ns : nutils.function.Namespace + Namespace containing the ``phi`` and ``x`` symbols. + degree_phi : int + Polynomial degree of the phase field basis. + lam : float + Diffuse interface width passed to the analytical formula. + r : float + Grain radius passed to the analytical formula. + + Returns + ------- + numpy.ndarray + DOF coefficient vector for the projected phase field. + """ phi_ini = MicroSimulation._analytical_phasefield(ns.x[0], ns.x[1], r, lam) sqrphi = topo.integral((ns.phi - phi_ini) ** 2, degree=degree_phi * 2) solphi = solver.optimize('solphi', sqrphi, droptol=1E-12) @@ -132,23 +205,71 @@ def _get_analytical_phasefield(topo, ns, degree_phi, lam, r): # export.vtk("micro-heat-{}".format(self._sim_id), bezier.tri, x, T=u, phi=phi) def get_state(self): - return [self._solphi.copy(), deepcopy(self._topo)] + """ + Return the current simulation state for checkpointing or state transfer. + + Returns + ------- + list + ``[solphi, topo, psi_nm1, k_nm1]`` — phase field DOF vector (copy), + the current refined topology (deep copy), average porosity, and + effective conductivity tensor from the last time step. + """ + return [self._solphi.copy(), deepcopy(self._topo), self._psi_nm1, self._k_nm1] def set_state(self, state): + """ + Restore the simulation state from a checkpoint or a donor simulation. + + Also rebuilds the nutils namespace for the restored topology and marks + the initial condition as set so that subsequent :meth:`solve` calls use + the correct (projection-based) mesh-refinement branch. + + Parameters + ---------- + state : list + State list as returned by :meth:`get_state`: + ``[solphi, topo, psi_nm1, k_nm1]``. + """ self._solphi = state[0] self._topo = state[1] + self._psi_nm1 = state[2] + self._k_nm1 = state[3] + self._initial_condition_is_set = True # state comes from a fully initialized sim, use the 'else' branch in _refine_mesh self._reinitialize_namespace(self._topo) # The namespace also needs to reloaded to its earlier state def _refine_mesh(self, topo_nm1, solphi_nm1): """ - At the time of the calling of this function a predicted solution exists in ns.phi + Adaptively refine the mesh around the grain interface. + + Two code paths exist: + + * **Initial refinement** (``_initial_condition_is_set = False``): starts + from the coarse topology and uses the analytical phase field projected + onto the coarse basis (``coarsesolphi``) to drive refinement. + * **Subsequent refinement** (``_initial_condition_is_set = True``): starts + from the coarse topology but guides refinement with the previous + refined solution (``solphi``), then projects it onto the new refined mesh. + + Parameters + ---------- + topo_nm1 : nutils.topology.Topology + Refined topology from the previous time step (used in the projection step). + solphi_nm1 : numpy.ndarray + Phase field DOF vector on ``topo_nm1``. + + Returns + ------- + topo : nutils.topology.HierarchicalTopology + Newly refined topology. + solphi : numpy.ndarray + Phase field DOF vector expressed on the new ``topo``. """ if not self._initial_condition_is_set: solphi = solphi_nm1 # ----- Refine the coarse mesh according to the projected solution to get a predicted refined topology ---- topo = self._topo_coarse - for level in range(self._ref_level): - # print("refinement level = {}".format(level)) + for _ in range(self._ref_level): smpl = topo.sample('uniform', 5) ielem, criterion = smpl.eval([topo.f_index, abs(self._ns.coarsephi - .5) < .4], coarsesolphi=solphi) @@ -165,8 +286,7 @@ def _refine_mesh(self, topo_nm1, solphi_nm1): else: # ----- Refine the coarse mesh according to the projected solution to get a predicted refined topology ---- topo = self._topo_coarse - for level in range(self._ref_level): - # print("refinement level = {}".format(level)) + for _ in range(self._ref_level): topo_union1 = topo_nm1 & topo smpl = topo_union1.sample('uniform', 5) ielem, criterion = smpl.eval([topo.f_index, abs(self._ns.phi - .5) < .4], solphi=solphi_nm1) @@ -190,8 +310,26 @@ def _refine_mesh(self, topo_nm1, solphi_nm1): def _solve_allen_cahn(self, topo, phi_coeffs_nm1, concentration, dt): """ - Solving the Allen-Cahn equation using a Newton solver. - Returns porosity of the micro domain. + Solve the Allen-Cahn phase field equation for one time step. + + Uses a fully implicit Newton iteration. The reaction term is driven by + the macro-scale concentration field. + + Parameters + ---------- + topo : nutils.topology.Topology + Current refined topology. + phi_coeffs_nm1 : numpy.ndarray + Phase field DOF vector at the previous time step (initial guess). + concentration : float + Macro-scale concentration value at this micro simulation location. + dt : float + Time step size. + + Returns + ------- + numpy.ndarray + Updated phase field DOF vector at the current time step. """ self._first_iter_done = True resphi = topo.integral('(lam^2 phibasis_n dphidt + gam phibasis_n ddwpdphi + gam lam^2 phibasis_n,i phi_,i + ' @@ -203,14 +341,47 @@ def _solve_allen_cahn(self, topo, phi_coeffs_nm1, concentration, dt): return phi_coeffs def _get_avg_porosity(self, topo, phi_coeffs): + """ + Compute the volume-averaged porosity of the micro domain. + + Porosity is defined as the integral of the phase field ``phi`` over the + unit cell (phi = 1 inside the grain, 0 outside). + + Parameters + ---------- + topo : nutils.topology.Topology + Current topology. + phi_coeffs : numpy.ndarray + Phase field DOF vector. + + Returns + ------- + float + Average porosity value in [0, 1]. + """ psi = topo.integral('phi d:x' @ self._ns, degree=self._degree_phi * 2).eval(solphi=phi_coeffs) return psi def _solve_heat_cell_problem(self, topo, phi_coeffs): """ - Solving the P1 homogenized heat equation - Returns upscaled conductivity for the micro domain + Solve the periodic cell problem for heat conduction homogenisation. + + Computes the corrector field ``u`` by solving the linear cell problem + arising from first-order homogenisation of the heat equation on the + micro domain with a phase-field-dependent conductivity. + + Parameters + ---------- + topo : nutils.topology.Topology + Current topology. + phi_coeffs : numpy.ndarray + Phase field DOF vector. + + Returns + ------- + numpy.ndarray + DOF vector for the corrector field ``u``. """ res = topo.integral('((phi ks + (1 - phi) kg) u_i,j ubasis_ni,j - ' '(ks - kg) phi_,j $_ij ubasis_ni) d:x' @ self._ns, degree=self._degree_u * 2) @@ -221,6 +392,27 @@ def _solve_heat_cell_problem(self, topo, phi_coeffs): return u_coeffs def _get_eff_conductivity(self, topo, u_coeffs, phi_coeffs): + """ + Compute the effective (upscaled) conductivity tensor. + + Evaluates the homogenised conductivity by integrating the cell-problem + solution ``u`` together with the phase-field-weighted local conductivity + over the micro domain. + + Parameters + ---------- + topo : nutils.topology.Topology + Current topology. + u_coeffs : numpy.ndarray + Corrector field DOF vector from :meth:`_solve_heat_cell_problem`. + phi_coeffs : numpy.ndarray + Phase field DOF vector. + + Returns + ------- + numpy.ndarray + 2x2 effective conductivity tensor as a dense array. + """ b = topo.integral( self._ns.eval_ij('(phi ks + (1 - phi) kg) ($_ij + du_ij) d:x'), degree=self._degree_u * @@ -231,6 +423,28 @@ def _get_eff_conductivity(self, topo, u_coeffs, phi_coeffs): return b.export("dense") def solve(self, macro_data, dt): + """ + Advance the micro simulation by one time step. + + Refines the mesh, evolves the phase field via the Allen-Cahn equation, + and solves the cell problem to obtain updated effective conductivity and + porosity. If the porosity has reached 0.95 (grain fully dissolved), the + simulation is frozen and the last computed values are returned. + + Parameters + ---------- + macro_data : dict + Dictionary containing macro-scale input data. Must include the key + ``"Concentration"`` with the macro concentration at this location. + dt : float + Time step size. + + Returns + ------- + dict + Output data with keys ``K00``, ``K11``, ``Porosity``, and + ``Grain-Size``. + """ if self._psi_nm1 < 0.95: topo, solphi = self._refine_mesh(self._topo, self._solphi) self._reinitialize_namespace(topo) @@ -256,9 +470,12 @@ def solve(self, macro_data, dt): output_data = dict() output_data["K00"] = k[0][0] - output_data["K01"] = k[0][1] - output_data["K10"] = k[1][0] output_data["K11"] = k[1][1] + + # Off-diagonal terms are not sent back as they are zero in this case + # output_data["K01"] = k[0][1] + # output_data["K10"] = k[1][0] + output_data["Porosity"] = psi output_data["Grain-Size"] = math.sqrt((1 - psi) / math.pi) @@ -266,6 +483,12 @@ def solve(self, macro_data, dt): def main(): + """ + Standalone driver for a single micro simulation (used for testing / debugging). + + Creates one :class:`MicroSimulation` instance, initialises it, and runs two + solve steps with prescribed concentration values. + """ micro_problem = MicroSimulation(0) dt = 1e-3 micro_problem.initialize() diff --git a/two-scale-heat-conduction/precice-config.xml b/two-scale-heat-conduction/precice-config.xml index 3ca95ad2f..028d0bf31 100644 --- a/two-scale-heat-conduction/precice-config.xml +++ b/two-scale-heat-conduction/precice-config.xml @@ -10,8 +10,6 @@ - - @@ -21,8 +19,6 @@ - - @@ -34,19 +30,15 @@ - - - + - - @@ -63,8 +55,6 @@ - -